Merge pull request #4697 from gentslava/feat/domain-enable-disable

feat(domains): add toggle to enable/disable domains
This commit is contained in:
Narciso E. Núñez Arias 2026-08-11 13:30:53 -04:00 committed by GitHub
commit 56fdb6151d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 9637 additions and 52 deletions

View File

@ -33,6 +33,7 @@ const makeDomain = (serviceName: string) =>
https: false,
uniqueConfigKey: 1,
port: 3000,
enabled: true,
}) as any;
// If the returned shell fragment is safe, parse() yields only string tokens.

View File

@ -0,0 +1,312 @@
import type { Compose } from "@dokploy/server/services/compose";
import type { Domain } from "@dokploy/server/services/domain";
import { addDomainToCompose } from "@dokploy/server/utils/docker/domain";
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 baseComposeYaml = `
services:
frigate:
image: frigate
`;
let composeYaml = baseComposeYaml;
vi.mock("node:fs", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:fs")>();
return {
...actual,
existsSync: vi.fn(() => true),
readFileSync: vi.fn(() => composeYaml),
};
});
const baseCompose = {
appName: "test-app",
composeType: "docker-compose",
composePath: "docker-compose.yml",
sourceType: "raw",
serverId: null,
isolatedDeployment: false,
randomize: false,
suffix: "",
} as unknown as Compose;
const baseDomain: Domain = {
host: "frigate.example.com",
port: 8971,
customEntrypoint: null,
https: false,
uniqueConfigKey: 1,
customCertResolver: null,
certificateType: "none",
applicationId: "",
composeId: "compose-id",
domainType: "compose",
serviceName: "frigate",
domainId: "domain-id",
path: "/",
createdAt: "",
previewDeploymentId: "",
internalPath: "/",
stripPath: false,
middlewares: null,
forwardAuthEnabled: false,
enabled: true,
};
const serviceLabels = (
result: Awaited<ReturnType<typeof addDomainToCompose>>,
) => (result?.services?.frigate?.labels as string[] | undefined) ?? [];
describe("addDomainToCompose enabled filtering", () => {
beforeEach(() => {
vi.clearAllMocks();
composeYaml = baseComposeYaml;
});
it("generates traefik labels for an enabled domain", async () => {
const result = await addDomainToCompose(baseCompose, [
{ ...baseDomain, enabled: true },
]);
const labels = serviceLabels(result);
expect(labels).toContain("traefik.enable=true");
expect(labels.some((l) => l.includes("Host(`frigate.example.com`)"))).toBe(
true,
);
});
it("skips a disabled domain entirely (no traefik labels)", async () => {
const result = await addDomainToCompose(baseCompose, [
{ ...baseDomain, enabled: false },
]);
const labels = serviceLabels(result);
expect(labels).not.toContain("traefik.enable=true");
expect(labels.some((l) => l.includes("Host(`frigate.example.com`)"))).toBe(
false,
);
});
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.each([
[
"docker-compose",
`services:
legacy:
image: frigate
labels:
- traefik.http.routers.test-app-1-web.rule=Host(\`frigate.example.com\`)
- traefik.http.services.test-app-1-web.loadbalancer.server.port=8971
- custom.label=preserved
frigate:
image: frigate
`,
],
[
"stack",
`services:
legacy:
image: frigate
deploy:
labels:
- traefik.http.routers.test-app-1-web.rule=Host(\`frigate.example.com\`)
- traefik.http.services.test-app-1-web.loadbalancer.server.port=8971
- custom.label=preserved
frigate:
image: frigate
`,
],
] as const)(
"removes stale labels from the previous %s service after reassignment",
async (composeType, staleComposeYaml) => {
composeYaml = staleComposeYaml;
const result = await addDomainToCompose({ ...baseCompose, composeType }, [
{ ...baseDomain, serviceName: "frigate", enabled: false },
]);
const previousService = result?.services?.legacy;
const labels =
composeType === "docker-compose"
? previousService?.labels
: previousService?.deploy?.labels;
expect(labels).toContain("custom.label=preserved");
expect(
(labels as string[]).some((label) => label.includes("test-app-1")),
).toBe(false);
},
);
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 mapping 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).toMatchObject({ "custom.label": "preserved" });
expect(
Object.keys(labels ?? {}).some((label) => label.includes("test-app-1")),
).toBe(false);
},
);
it.each([
[
"docker-compose",
`services:
frigate:
image: frigate
labels:
traefik.enable: "true"
traefik.http.routers.test-app-1-web.rule: Host(\`old.example.com\`)
traefik.http.services.test-app-1-web.loadbalancer.server.port: 8971
custom.label: preserved
`,
"traefik.docker.network",
],
[
"stack",
`services:
frigate:
image: frigate
deploy:
labels:
traefik.enable: "true"
traefik.http.routers.test-app-1-web.rule: Host(\`old.example.com\`)
traefik.http.services.test-app-1-web.loadbalancer.server.port: 8971
custom.label: preserved
`,
"traefik.swarm.network",
],
] as const)(
"regenerates routing in mapping labels for an enabled domain in %s",
async (composeType, mappingComposeYaml, networkLabel) => {
composeYaml = mappingComposeYaml;
const result = await addDomainToCompose({ ...baseCompose, composeType }, [
{ ...baseDomain, enabled: true },
]);
const service = result?.services?.frigate;
const labels =
composeType === "docker-compose"
? service?.labels
: service?.deploy?.labels;
expect(labels).toMatchObject({
"custom.label": "preserved",
"traefik.enable": "true",
[networkLabel]: "dokploy-network",
"traefik.http.routers.test-app-1-web.rule":
"Host(`frigate.example.com`)",
"traefik.http.services.test-app-1-web.loadbalancer.server.port": "8971",
});
},
);
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 },
{
...baseDomain,
host: "disabled.example.com",
uniqueConfigKey: 2,
enabled: false,
},
]);
const labels = serviceLabels(result);
expect(labels.some((l) => l.includes("Host(`enabled.example.com`)"))).toBe(
true,
);
expect(labels.some((l) => l.includes("Host(`disabled.example.com`)"))).toBe(
false,
);
});
});

View File

@ -35,6 +35,7 @@ describe("Host rule format regression tests", () => {
customEntrypoint: null,
middlewares: null,
forwardAuthEnabled: false,
enabled: true,
};
describe("Host rule format validation", () => {

View File

@ -24,6 +24,7 @@ describe("createDomainLabels", () => {
stripPath: false,
middlewares: null,
forwardAuthEnabled: false,
enabled: true,
};
it("should create basic labels for web entrypoint", async () => {

View File

@ -35,6 +35,7 @@ const baseDomain: Domain = {
stripPath: false,
middlewares: null,
forwardAuthEnabled: false,
enabled: true,
};
describe("forwardAuthMiddlewareName", () => {

View File

@ -151,6 +151,7 @@ const baseDomain: Domain = {
stripPath: false,
middlewares: null,
forwardAuthEnabled: false,
enabled: true,
};
const baseRedirect: Redirect = {

View File

@ -14,6 +14,7 @@ import Link from "next/link";
import { DialogAction } from "@/components/shared/dialog-action";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import {
Tooltip,
TooltipContent,
@ -35,7 +36,9 @@ interface ColumnsProps {
validationStates: ValidationStates;
handleValidateDomain: (host: string) => Promise<void>;
handleDeleteDomain: (domainId: string) => Promise<void>;
handleToggleEnable: (domainId: string) => Promise<void>;
isDeleting: boolean;
isToggling: boolean;
serverIp?: string;
canCreateDomain: boolean;
canDeleteDomain: boolean;
@ -47,7 +50,9 @@ export const createColumns = ({
validationStates,
handleValidateDomain,
handleDeleteDomain,
handleToggleEnable,
isDeleting,
isToggling,
serverIp,
canCreateDomain,
canDeleteDomain,
@ -249,6 +254,42 @@ export const createColumns = ({
);
},
},
{
id: "status",
header: "Status",
cell: ({ row }) => {
const domain = row.original;
if (!canCreateDomain) {
return (
<Badge variant={domain.enabled ? "outline" : "secondary"}>
{domain.enabled ? "Enabled" : "Disabled"}
</Badge>
);
}
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center">
<Switch
checked={domain.enabled}
onCheckedChange={() => handleToggleEnable(domain.domainId)}
disabled={isToggling}
/>
</div>
</TooltipTrigger>
<TooltipContent>
<p>
{domain.enabled
? "Domain is active. Toggle to disable routing without deleting it."
: "Domain is disabled and not routed. Toggle to enable it again."}
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
},
},
{
id: "actions",
header: "Actions",

View File

@ -46,6 +46,7 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api } from "@/utils/api";
import { COMPOSE_REDEPLOY_TOAST, ComposeRedeployAlert } from "./redeploy-hint";
export type CacheType = "fetch" | "cache";
@ -300,7 +301,12 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
customEntrypoint: data.useCustomEntrypoint ? data.customEntrypoint : null,
})
.then(async () => {
toast.success(dictionary.success);
toast.success(
dictionary.success,
data.domainType === "compose"
? { description: COMPOSE_REDEPLOY_TOAST }
: undefined,
);
if (data.domainType === "application") {
await utils.domain.byApplicationId.invalidate({
@ -337,12 +343,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
{type === "compose" && (
<AlertBlock type="info" className="mb-4">
Whenever you make changes to domains, remember to redeploy your
compose to apply the changes.
</AlertBlock>
)}
{type === "compose" && <ComposeRedeployAlert className="mb-4" />}
<Form {...form}>
<form

View File

@ -0,0 +1,19 @@
import { AlertBlock } from "@/components/shared/alert-block";
/**
* Domains attached to a compose service are rendered as docker labels and only
* take effect on the next deployment. These strings keep the "redeploy required"
* wording consistent across the add/edit dialog, the domains list and the
* toasts shown after create/update/delete/toggle operations.
*/
export const COMPOSE_REDEPLOY_HINT =
"Whenever you make changes to domains, remember to redeploy your compose to apply the changes.";
export const COMPOSE_REDEPLOY_TOAST =
"Redeploy the compose to apply the changes.";
export const ComposeRedeployAlert = ({ className }: { className?: string }) => (
<AlertBlock type="info" className={className}>
{COMPOSE_REDEPLOY_HINT}
</AlertBlock>
);

View File

@ -44,6 +44,7 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import {
Table,
TableBody,
@ -58,11 +59,13 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { createColumns } from "./columns";
import { DnsHelperModal } from "./dns-helper-modal";
import { AddDomain } from "./handle-domain";
import { HandleForwardAuth } from "./handle-forward-auth";
import { COMPOSE_REDEPLOY_TOAST, ComposeRedeployAlert } from "./redeploy-hint";
export type ValidationState = {
isLoading: boolean;
@ -146,12 +149,34 @@ export const ShowDomains = ({ id, type }: Props) => {
api.domain.validateDomain.useMutation();
const { mutateAsync: deleteDomain, isPending: isRemoving } =
api.domain.delete.useMutation();
const { mutateAsync: toggleEnable, isPending: isToggling } =
api.domain.toggleEnable.useMutation();
const handleToggleEnable = async (domainId: string) => {
try {
const result = await toggleEnable({ domainId });
refetch();
toast.success(
result.enabled ? "Domain enabled" : "Domain disabled",
result.requiresRedeploy
? { description: COMPOSE_REDEPLOY_TOAST }
: undefined,
);
} catch {
toast.error("Error updating the domain");
}
};
const handleDeleteDomain = async (domainId: string) => {
try {
await deleteDomain({ domainId });
refetch();
toast.success("Domain deleted successfully");
toast.success(
"Domain deleted successfully",
type === "compose"
? { description: COMPOSE_REDEPLOY_TOAST }
: undefined,
);
} catch {
toast.error("Error deleting domain");
}
@ -200,7 +225,9 @@ export const ShowDomains = ({ id, type }: Props) => {
validationStates,
handleValidateDomain,
handleDeleteDomain,
handleToggleEnable,
isDeleting: isRemoving,
isToggling,
serverIp: application?.server?.ipAddress?.toString() || ip?.toString(),
canCreateDomain,
canDeleteDomain,
@ -265,6 +292,11 @@ export const ShowDomains = ({ id, type }: Props) => {
)}
</div>
</CardHeader>
{type === "compose" && data && data.length > 0 && (
<div className="px-6 pb-4">
<ComposeRedeployAlert />
</div>
)}
<CardContent className="flex w-full flex-row gap-4">
{isLoadingDomains ? (
<div className="flex w-full flex-row gap-4 min-h-[40vh] justify-center items-center">
@ -413,7 +445,10 @@ export const ShowDomains = ({ id, type }: Props) => {
return (
<Card
key={item.domainId}
className="relative overflow-hidden w-full border transition-all hover:shadow-md bg-transparent h-fit"
className={cn(
"relative overflow-hidden w-full border transition-all hover:shadow-md bg-transparent h-fit",
!item.enabled && "opacity-60",
)}
>
<CardContent className="p-6">
<div className="flex flex-col gap-4">
@ -466,18 +501,7 @@ export const ShowDomains = ({ id, type }: Props) => {
description="Are you sure you want to delete this domain?"
type="destructive"
onClick={async () => {
await deleteDomain({
domainId: item.domainId,
})
.then((_data) => {
refetch();
toast.success(
"Domain deleted successfully",
);
})
.catch(() => {
toast.error("Error deleting domain");
});
await handleDeleteDomain(item.domainId);
}}
>
<Button
@ -492,15 +516,41 @@ export const ShowDomains = ({ id, type }: Props) => {
)}
</div>
</div>
<div className="w-full break-all">
<Link
className="flex items-center gap-2 text-base font-medium hover:underline"
target="_blank"
href={`${item.https ? "https" : "http"}://${item.host}${item.path}`}
>
{item.host}
<ExternalLink className="size-4 min-w-4" />
</Link>
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="w-full break-all">
<Link
className="flex items-center gap-2 text-base font-medium hover:underline"
target="_blank"
href={`${item.https ? "https" : "http"}://${item.host}${item.path}`}
>
{item.host}
<ExternalLink className="size-4 min-w-4" />
</Link>
</div>
{canCreateDomain && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center shrink-0">
<Switch
checked={item.enabled}
onCheckedChange={() =>
handleToggleEnable(item.domainId)
}
disabled={isToggling}
/>
</div>
</TooltipTrigger>
<TooltipContent>
<p>
{item.enabled
? "Domain is active. Toggle to disable routing without deleting it."
: "Domain is disabled and not routed. Toggle to enable it again."}
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
{/* Domain Details */}

View File

@ -0,0 +1 @@
ALTER TABLE "domain" ADD COLUMN "enabled" boolean DEFAULT true NOT NULL;

File diff suppressed because it is too large Load Diff

View File

@ -1282,6 +1282,13 @@
"when": 1786435879438,
"tag": "0182_skinny_wild_pack",
"breakpoints": true
},
{
"idx": 183,
"version": "7",
"when": 1786463845579,
"tag": "0183_tearful_groot",
"breakpoints": true
}
]
}

View File

@ -141,6 +141,58 @@ export const domainRouter = createTRPCRouter({
}
return result;
}),
toggleEnable: protectedProcedure
.input(apiFindDomain)
.mutation(async ({ input, ctx }) => {
const currentDomain = await findDomainById(input.domainId);
const serviceId = currentDomain.applicationId || currentDomain.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
domain: ["create"],
});
} else if (currentDomain.previewDeploymentId) {
const preview = await findPreviewDeploymentById(
currentDomain.previewDeploymentId,
);
await checkServicePermissionAndAccess(ctx, preview.applicationId, {
domain: ["create"],
});
}
const result = await updateDomainById(input.domainId, {
enabled: !currentDomain.enabled,
});
const domain = await findDomainById(input.domainId);
await audit(ctx, {
action: "update",
resourceType: "domain",
resourceId: domain.domainId,
resourceName: domain.host,
});
// Applications apply instantly through the traefik file provider:
// manageDomain creates the router when enabled and removes it when
// disabled. Compose domains are docker labels and only change on the
// next deployment, so we just persist the flag here.
if (domain.applicationId) {
const application = await findApplicationById(domain.applicationId);
await manageDomain(application, domain);
} else if (domain.previewDeploymentId) {
const previewDeployment = await findPreviewDeploymentById(
domain.previewDeploymentId,
);
const application = await findApplicationById(
previewDeployment.applicationId,
);
application.appName = previewDeployment.appName;
await manageDomain(application, domain);
}
return {
...result,
requiresRedeploy: domain.domainType === "compose",
};
}),
one: protectedProcedure.input(apiFindDomain).query(async ({ input, ctx }) => {
const domain = await findDomainById(input.domainId);
const serviceId = domain.applicationId || domain.composeId;

View File

@ -56,6 +56,7 @@ 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),
enabled: boolean("enabled").notNull().default(true),
});
export const domainsRelations = relations(domains, ({ one }) => ({
@ -129,5 +130,6 @@ export const apiUpdateDomain = createSchema
stripPath: true,
middlewares: true,
forwardAuthEnabled: true,
enabled: true,
})
.merge(createSchema.pick({ domainId: true }).required());

View File

@ -170,6 +170,32 @@ export const applyComposeFilePatch = async (
}
};
const removeDomainLabels = (
labels: DefinitionsService["labels"],
appName: string,
uniqueConfigKey: number,
) => {
const prefixes = [
`traefik.http.routers.${appName}-${uniqueConfigKey}-`,
`traefik.http.services.${appName}-${uniqueConfigKey}-`,
`traefik.http.middlewares.stripprefix-${appName}-${uniqueConfigKey}.`,
`traefik.http.middlewares.addprefix-${appName}-${uniqueConfigKey}.`,
];
const belongsToDomain = (label: string) =>
prefixes.some((prefix) => label.startsWith(prefix));
if (Array.isArray(labels)) {
return labels.filter((label) => !belongsToDomain(label));
}
if (labels) {
return Object.fromEntries(
Object.entries(labels).filter(([label]) => !belongsToDomain(label)),
);
}
return labels;
};
export const addDomainToCompose = async (
compose: Compose,
domains: Domain[],
@ -203,6 +229,24 @@ export const addDomainToCompose = async (
}
for (const domain of domains) {
for (const service of Object.values(result.services ?? {})) {
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) {
throw new Error(`Domain "${domain.host}" is missing a service name`);
@ -242,33 +286,31 @@ export const addDomainToCompose = async (
labels = result.services[serviceName].deploy.labels;
}
const networkLabel =
compose.composeType === "docker-compose"
? "traefik.docker.network"
: "traefik.swarm.network";
const networkName = compose.isolatedDeployment
? compose.suffix || compose.appName
: "dokploy-network";
if (Array.isArray(labels)) {
if (!labels.includes("traefik.enable=true")) {
labels.unshift("traefik.enable=true");
}
labels.unshift(...httpLabels);
if (!compose.isolatedDeployment) {
if (compose.composeType === "docker-compose") {
if (!labels.includes("traefik.docker.network=dokploy-network")) {
labels.unshift("traefik.docker.network=dokploy-network");
}
} else {
// Stack Case
if (!labels.includes("traefik.swarm.network=dokploy-network")) {
labels.unshift("traefik.swarm.network=dokploy-network");
}
}
} else {
const isolatedNetwork = compose.suffix || compose.appName;
if (compose.composeType === "docker-compose") {
if (!labels.includes(`traefik.docker.network=${isolatedNetwork}`)) {
labels.unshift(`traefik.docker.network=${isolatedNetwork}`);
}
} else {
if (!labels.includes(`traefik.swarm.network=${isolatedNetwork}`)) {
labels.unshift(`traefik.swarm.network=${isolatedNetwork}`);
}
}
const networkLabelEntry = `${networkLabel}=${networkName}`;
if (!labels.includes(networkLabelEntry)) {
labels.unshift(networkLabelEntry);
}
} else if (labels) {
labels["traefik.enable"] = "true";
labels[networkLabel] = networkName;
for (const label of httpLabels) {
const separatorIndex = label.indexOf("=");
labels[label.slice(0, separatorIndex)] = label.slice(
separatorIndex + 1,
);
}
}

View File

@ -19,6 +19,15 @@ import { createPathMiddlewares, removePathMiddlewares } from "./middleware";
export const manageDomain = async (app: ApplicationNested, domain: Domain) => {
const { appName } = app;
// A disabled domain keeps its configuration in the database but must never
// expose a traefik router. Guarding here covers every caller (create, update,
// forward-auth, toggle) so a disabled domain can't be revived from any path.
if (!domain.enabled) {
await removeDomain(app, domain.uniqueConfigKey);
return;
}
let config: FileConfig;
if (app.serverId) {