Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ describe("Host rule format regression tests", () => {
customEntrypoint: null,
middlewares: null,
forwardAuthEnabled: false,
skipDefaultNetwork: false,
};

describe("Host rule format validation", () => {
Expand Down
1 change: 1 addition & 0 deletions apps/dokploy/__test__/compose/domain/labels.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ describe("createDomainLabels", () => {
stripPath: false,
middlewares: null,
forwardAuthEnabled: false,
skipDefaultNetwork: false,
};

it("should create basic labels for web entrypoint", async () => {
Expand Down
30 changes: 30 additions & 0 deletions apps/dokploy/__test__/compose/domain/network-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,34 @@ describe("addDokployNetworkToService", () => {
const result = addDokployNetworkToService(["default", "dokploy-network"]);
expect(result).toEqual(["default", "dokploy-network"]);
});

describe("skipDefaultNetwork option", () => {
it("should still add dokploy-network but skip default when true (array)", () => {
const result = addDokployNetworkToService(["dokploy-network"], true);
expect(result).toEqual(["dokploy-network"]);
});

it("should skip default on an empty array when true", () => {
const result = addDokployNetworkToService([], true);
expect(result).toEqual(["dokploy-network"]);
});

it("should preserve other existing networks while skipping default", () => {
const result = addDokployNetworkToService(["other-network"], true);
expect(result).toEqual(["other-network", "dokploy-network"]);
});

it("should still add dokploy-network but skip default when true (object)", () => {
const result = addDokployNetworkToService({ "other-network": {} }, true);
expect(result).toEqual({
"other-network": {},
"dokploy-network": {},
});
});

it("should default to false and preserve existing behavior when omitted", () => {
const result = addDokployNetworkToService(["dokploy-network"]);
expect(result).toEqual(["dokploy-network", "default"]);
});
});
});
1 change: 1 addition & 0 deletions apps/dokploy/__test__/traefik/forward-auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const baseDomain: Domain = {
stripPath: false,
middlewares: null,
forwardAuthEnabled: false,
skipDefaultNetwork: false,
};

describe("forwardAuthMiddlewareName", () => {
Expand Down
1 change: 1 addition & 0 deletions apps/dokploy/__test__/traefik/traefik.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ const baseDomain: Domain = {
stripPath: false,
middlewares: null,
forwardAuthEnabled: false,
skipDefaultNetwork: false,
};

const baseRedirect: Redirect = {
Expand Down
1 change: 1 addition & 0 deletions apps/dokploy/drizzle/0175_skip_default_network.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE "domain" ADD COLUMN "skipDefaultNetwork" boolean DEFAULT false NOT NULL;
7 changes: 7 additions & 0 deletions apps/dokploy/drizzle/meta/_journal.json
Original file line number Diff line number Diff line change
Expand Up @@ -1226,6 +1226,13 @@
"when": 1783674181297,
"tag": "0174_great_naoko",
"breakpoints": true
},
{
"idx": 175,
"version": "7",
"when": 1783860223772,
"tag": "0175_skip_default_network",
"breakpoints": true
}
]
}
12 changes: 12 additions & 0 deletions packages/server/src/db/schema/domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,16 @@ export const domains = pgTable("domain", {
stripPath: boolean("stripPath").notNull().default(false),
middlewares: text("middlewares").array().default(sql`ARRAY[]::text[]`),
forwardAuthEnabled: boolean("forwardAuthEnabled").notNull().default(false),
// When a domain is attached to a compose service, Dokploy always attaches
// that service to BOTH "dokploy-network" and the project's implicit
// "default" network (see addDokployNetworkToService) so it keeps talking
// to sibling services (fix for #3562). For single-service compose apps
// (or any service that doesn't need default-network connectivity), being
// on two networks can make Traefik's Docker provider pick the wrong
// network's IP for routing, causing persistent 502s that don't self-heal.
// Opt-in escape hatch, defaults to false so existing behavior/tests are
// unchanged.
skipDefaultNetwork: boolean("skipDefaultNetwork").notNull().default(false),
});

export const domainsRelations = relations(domains, ({ one }) => ({
Expand Down Expand Up @@ -96,6 +106,7 @@ export const apiCreateDomain = createSchema.pick({
stripPath: true,
middlewares: true,
forwardAuthEnabled: true,
skipDefaultNetwork: true,
});

export const apiFindDomain = z.object({
Expand Down Expand Up @@ -129,5 +140,6 @@ export const apiUpdateDomain = createSchema
stripPath: true,
middlewares: true,
forwardAuthEnabled: true,
skipDefaultNetwork: true,
})
.merge(createSchema.pick({ domainId: true }).required());
1 change: 1 addition & 0 deletions packages/server/src/db/validations/domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export const domainCompose = z
customCertResolver: z.string(),
serviceName: z.string().min(1, { message: "Service name is required" }),
middlewares: z.array(z.string()).optional(),
skipDefaultNetwork: z.boolean().optional(),
})
.superRefine((input, ctx) => {
if (input.https && !input.certificateType) {
Expand Down
14 changes: 12 additions & 2 deletions packages/server/src/utils/docker/domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ export const addDomainToCompose = async (
// Add the dokploy-network to the service
result.services[serviceName].networks = addDokployNetworkToService(
result.services[serviceName].networks,
domain.skipDefaultNetwork ?? false,
);
}
}
Expand Down Expand Up @@ -349,6 +350,15 @@ export const createDomainLabels = (

export const addDokployNetworkToService = (
networkService: DefinitionsService["networks"],
// Opt-in escape hatch for #<issue-number>: attaching a domain-linked
// service to both dokploy-network AND the implicit "default" network
// (added in #3562 so sibling services stay reachable) can make Traefik's
// Docker provider pick the wrong network's IP for routing, causing 502s
// that don't self-heal on restart. Services that don't rely on the
// default network for inter-service connectivity (e.g. single-service
// compose apps) can set this to skip the "default" attachment. Defaults
// to false so existing behavior is unchanged.
skipDefaultNetwork = false,
) => {
let networks = networkService;
const network = "dokploy-network";
Expand All @@ -361,14 +371,14 @@ export const addDokployNetworkToService = (
if (!networks.includes(network)) {
networks.push(network);
}
if (!networks.includes(defaultNetwork)) {
if (!skipDefaultNetwork && !networks.includes(defaultNetwork)) {
networks.push(defaultNetwork);
}
} else if (networks && typeof networks === "object") {
if (!(network in networks)) {
networks[network] = {};
}
if (!(defaultNetwork in networks)) {
if (!skipDefaultNetwork && !(defaultNetwork in networks)) {
networks[defaultNetwork] = {};
}
}
Expand Down