diff --git a/apps/dokploy/__test__/compose/domain/enabled-filter.test.ts b/apps/dokploy/__test__/compose/domain/enabled-filter.test.ts index 329d4ef4b..bdb5d3e9f 100644 --- a/apps/dokploy/__test__/compose/domain/enabled-filter.test.ts +++ b/apps/dokploy/__test__/compose/domain/enabled-filter.test.ts @@ -6,11 +6,12 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; // addDomainToCompose reads the compose file from disk through loadDockerCompose // (existsSync + readFileSync). Mock node:fs so the function runs its real // label-generation logic against an in-memory compose spec. -const composeYaml = ` +const baseComposeYaml = ` services: frigate: image: frigate `; +let composeYaml = baseComposeYaml; vi.mock("node:fs", async (importOriginal) => { const actual = await importOriginal(); @@ -62,6 +63,7 @@ const serviceLabels = ( describe("addDomainToCompose enabled filtering", () => { beforeEach(() => { vi.clearAllMocks(); + composeYaml = baseComposeYaml; }); it("generates traefik labels for an enabled domain", async () => { @@ -88,6 +90,55 @@ describe("addDomainToCompose enabled filtering", () => { ); }); + it.each([ + [ + "docker-compose", + `services: + frigate: + image: frigate + labels: + - traefik.enable=true + - traefik.http.routers.test-app-1-web.rule=Host(\`frigate.example.com\`) + - traefik.http.services.test-app-1-web.loadbalancer.server.port=8971 + - traefik.http.middlewares.stripprefix-test-app-1.stripprefix.prefixes=/api + - custom.label=preserved +`, + ], + [ + "stack", + `services: + frigate: + image: frigate + deploy: + labels: + - traefik.enable=true + - traefik.http.routers.test-app-1-web.rule=Host(\`frigate.example.com\`) + - traefik.http.services.test-app-1-web.loadbalancer.server.port=8971 + - traefik.http.middlewares.stripprefix-test-app-1.stripprefix.prefixes=/api + - custom.label=preserved +`, + ], + ] as const)( + "removes stale labels for a disabled domain from %s rebuilds", + async (composeType, staleComposeYaml) => { + composeYaml = staleComposeYaml; + + const result = await addDomainToCompose({ ...baseCompose, composeType }, [ + { ...baseDomain, enabled: false }, + ]); + + const service = result?.services?.frigate; + const labels = + composeType === "docker-compose" + ? service?.labels + : service?.deploy?.labels; + expect(labels).toContain("custom.label=preserved"); + expect( + (labels as string[]).some((label) => label.includes("test-app-1")), + ).toBe(false); + }, + ); + it("emits labels only for the enabled domain when both are present", async () => { const result = await addDomainToCompose(baseCompose, [ { ...baseDomain, host: "enabled.example.com", enabled: true }, diff --git a/packages/server/src/utils/docker/domain.ts b/packages/server/src/utils/docker/domain.ts index ec66d350c..7c9dfb3dd 100644 --- a/packages/server/src/utils/docker/domain.ts +++ b/packages/server/src/utils/docker/domain.ts @@ -166,6 +166,25 @@ export const applyComposeFilePatch = async ( } }; +const removeDomainLabels = ( + labels: DefinitionsService["labels"], + appName: string, + uniqueConfigKey: number, +) => { + if (!Array.isArray(labels)) return labels; + + const prefixes = [ + `traefik.http.routers.${appName}-${uniqueConfigKey}-`, + `traefik.http.services.${appName}-${uniqueConfigKey}-`, + `traefik.http.middlewares.stripprefix-${appName}-${uniqueConfigKey}.`, + `traefik.http.middlewares.addprefix-${appName}-${uniqueConfigKey}.`, + ]; + + return labels.filter( + (label) => !prefixes.some((prefix) => label.startsWith(prefix)), + ); +}; + export const addDomainToCompose = async ( compose: Compose, domains: Domain[], @@ -198,7 +217,27 @@ export const addDomainToCompose = async ( result = randomized; } - // Disabled domains keep their config but must not produce any traefik labels. + for (const domain of domains.filter((d) => !d.enabled)) { + if (!domain.serviceName) continue; + + const service = result.services?.[domain.serviceName]; + if (!service) continue; + + if (compose.composeType === "docker-compose") { + service.labels = removeDomainLabels( + service.labels, + appName, + domain.uniqueConfigKey, + ); + } else if (service.deploy) { + service.deploy.labels = removeDomainLabels( + service.deploy.labels, + appName, + domain.uniqueConfigKey, + ); + } + } + for (const domain of domains.filter((d) => d.enabled)) { const { serviceName, https } = domain; if (!serviceName) {