diff --git a/apps/dokploy/__test__/compose/domain/host-rule-format.test.ts b/apps/dokploy/__test__/compose/domain/host-rule-format.test.ts index 6f843b8a81..dc33d8ae1f 100644 --- a/apps/dokploy/__test__/compose/domain/host-rule-format.test.ts +++ b/apps/dokploy/__test__/compose/domain/host-rule-format.test.ts @@ -19,6 +19,7 @@ describe("Host rule format regression tests", () => { host: "example.com", port: 8080, https: false, + enabled: true, uniqueConfigKey: 1, customCertResolver: null, certificateType: "none", diff --git a/apps/dokploy/__test__/compose/domain/labels.test.ts b/apps/dokploy/__test__/compose/domain/labels.test.ts index dce69cfe4f..324a40b605 100644 --- a/apps/dokploy/__test__/compose/domain/labels.test.ts +++ b/apps/dokploy/__test__/compose/domain/labels.test.ts @@ -9,6 +9,7 @@ describe("createDomainLabels", () => { port: 8080, customEntrypoint: null, https: false, + enabled: true, uniqueConfigKey: 1, customCertResolver: null, certificateType: "none", diff --git a/apps/dokploy/__test__/traefik/traefik.test.ts b/apps/dokploy/__test__/traefik/traefik.test.ts index 14d45f76c9..6f8c457eda 100644 --- a/apps/dokploy/__test__/traefik/traefik.test.ts +++ b/apps/dokploy/__test__/traefik/traefik.test.ts @@ -136,6 +136,7 @@ const baseDomain: Domain = { domainId: "", host: "", https: false, + enabled: true, path: null, port: null, customEntrypoint: null, diff --git a/apps/dokploy/components/dashboard/application/domains/columns.tsx b/apps/dokploy/components/dashboard/application/domains/columns.tsx index b88443dcc7..51ea018264 100644 --- a/apps/dokploy/components/dashboard/application/domains/columns.tsx +++ b/apps/dokploy/components/dashboard/application/domains/columns.tsx @@ -24,6 +24,8 @@ import type { RouterOutputs } from "@/utils/api"; import { DnsHelperModal } from "./dns-helper-modal"; import { AddDomain } from "./handle-domain"; import type { ValidationStates } from "./show-domains"; +import { Switch } from "@/components/ui/switch"; +import { cn } from "@/lib/utils"; export type Domain = | RouterOutputs["domain"]["byApplicationId"][0] @@ -39,6 +41,7 @@ interface ColumnsProps { serverIp?: string; canCreateDomain: boolean; canDeleteDomain: boolean; + handleToggleDomain: (domainId: string, enabled: boolean) => Promise; } export const createColumns = ({ @@ -47,6 +50,7 @@ export const createColumns = ({ validationStates, handleValidateDomain, handleDeleteDomain, + handleToggleDomain, isDeleting, serverIp, canCreateDomain, @@ -225,6 +229,24 @@ export const createColumns = ({ ); }, }, + { + accessorKey: "enabled", + header: "Enabled", + cell: ({ row }) => { + const domain = row.original; + return ( + { + await handleToggleDomain(domain.domainId, checked); + }} + className={cn("bg-white", { + "bg-muted-foreground": !domain.enabled, + })} + /> + ); + }, + }, { accessorKey: "createdAt", header: ({ column }) => { diff --git a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx index af8d691c0b..20e799e4f5 100644 --- a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx +++ b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx @@ -25,7 +25,7 @@ import { XCircle, } from "lucide-react"; import Link from "next/link"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import { toast } from "sonner"; import { DialogAction } from "@/components/shared/dialog-action"; import { Badge } from "@/components/ui/badge"; @@ -62,6 +62,8 @@ import { api } from "@/utils/api"; import { createColumns } from "./columns"; import { DnsHelperModal } from "./dns-helper-modal"; import { AddDomain } from "./handle-domain"; +import { Switch } from "@/components/ui/switch"; +import { cn } from "@/lib/utils"; export type ValidationState = { isLoading: boolean; @@ -80,6 +82,7 @@ interface Props { } export const ShowDomains = ({ id, type }: Props) => { + const { data: permissions } = api.user.getPermissions.useQuery(); const canCreateDomain = permissions?.domain.create ?? false; const canDeleteDomain = permissions?.domain.delete ?? false; @@ -145,6 +148,18 @@ export const ShowDomains = ({ id, type }: Props) => { api.domain.validateDomain.useMutation(); const { mutateAsync: deleteDomain, isPending: isRemoving } = api.domain.delete.useMutation(); + const { mutateAsync: updateDomain } = + api.domain.update.useMutation(); + + const handleToggleDomain = async (domainId: string, enabled: boolean) => { + try { + await updateDomain({ domainId, enabled }); + refetch(); + toast.success(`Domain ${enabled ? "enabled" : "disabled"} successfully`); + } catch { + toast.error(`Error ${enabled ? "enabling" : "disabling"} domain`); + } + }; const handleDeleteDomain = async (domainId: string) => { try { @@ -193,17 +208,30 @@ export const ShowDomains = ({ id, type }: Props) => { } }; - const columns = createColumns({ + const columns = useMemo(() => createColumns({ id, type, validationStates, handleValidateDomain, handleDeleteDomain, + handleToggleDomain, isDeleting: isRemoving, serverIp: application?.server?.ipAddress?.toString() || ip?.toString(), canCreateDomain, canDeleteDomain, - }); + }), [ + id, + type, + validationStates, + handleValidateDomain, + handleDeleteDomain, + handleToggleDomain, + isRemoving, + application?.server?.ipAddress, + ip, + canCreateDomain, + canDeleteDomain, + ]); const table = useReactTable({ data: data ?? [], @@ -424,7 +452,7 @@ export const ShowDomains = ({ id, type }: Props) => { {item.serviceName} )} -
+
{!item.host.includes("sslip.io") && ( { } /> )} + + + + + { + await handleToggleDomain(item.domainId, checked); + }} + className={cn("bg-white", { + "bg-muted-foreground": !item.enabled, + })} + /> + + +

Enable or disable the domain.

+
+
+
+ {canCreateDomain && ( { + const compose = await findComposeById(composeId); + const jobData: DeploymentJob = { + composeId: composeId, + titleLog: title, + descriptionLog: description, + type: "redeploy", + applicationType: "compose", + server: !!compose.serverId, + }; + if (IS_CLOUD && compose.serverId) { + jobData.serverId = compose.serverId; + deploy(jobData).catch((error) => { + console.error("Background deployment failed:", error); + }); + } else { + await myQueue.add( + "deployments", + { ...jobData }, + { + removeOnComplete: true, + removeOnFail: true, + }, + ); + } +}; + export const domainRouter = createTRPCRouter({ create: protectedProcedure .input(apiCreateDomain) @@ -52,6 +84,13 @@ export const domainRouter = createTRPCRouter({ resourceId: domain.domainId, resourceName: domain.host, }); + if (domain.composeId) { + await triggerComposeReload( + domain.composeId, + "Domain creation deployment", + `Domain ${domain.host} created` + ); + } return domain; } catch (error) { throw new TRPCError({ @@ -138,6 +177,12 @@ export const domainRouter = createTRPCRouter({ ); application.appName = previewDeployment.appName; await manageDomain(application, domain); + } else if (domain.composeId) { + await triggerComposeReload( + domain.composeId, + "Domain update deployment", + `Domain ${domain.host} updated` + ); } return result; }), @@ -187,6 +232,12 @@ export const domainRouter = createTRPCRouter({ if (domain.applicationId) { const application = await findApplicationById(domain.applicationId); await removeDomain(application, domain.uniqueConfigKey); + } else if (domain.composeId) { + await triggerComposeReload( + domain.composeId, + "Domain deletion deployment", + `Domain ${domain.host} deleted` + ); } return result; diff --git a/packages/server/src/db/schema/domain.ts b/packages/server/src/db/schema/domain.ts index 646dfdf9f2..6323de4a77 100644 --- a/packages/server/src/db/schema/domain.ts +++ b/packages/server/src/db/schema/domain.ts @@ -54,6 +54,7 @@ export const domains = pgTable("domain", { certificateType: certificateType("certificateType").notNull().default("none"), internalPath: text("internalPath").default("/"), stripPath: boolean("stripPath").notNull().default(false), + enabled: boolean("enabled").notNull().default(true), middlewares: text("middlewares").array().default(sql`ARRAY[]::text[]`), }); @@ -93,6 +94,7 @@ export const apiCreateDomain = createSchema.pick({ previewDeploymentId: true, internalPath: true, stripPath: true, + enabled: true, middlewares: true, }); @@ -125,6 +127,8 @@ export const apiUpdateDomain = createSchema domainType: true, internalPath: true, stripPath: true, + enabled: true, middlewares: true, }) + .partial() .merge(createSchema.pick({ domainId: true }).required()); diff --git a/packages/server/src/services/domain.ts b/packages/server/src/services/domain.ts index e1460fd482..1ba57e6dfe 100644 --- a/packages/server/src/services/domain.ts +++ b/packages/server/src/services/domain.ts @@ -58,7 +58,7 @@ export const generateTraefikMeDomain = async ( if (process.env.NODE_ENV === "development") { return generateRandomDomain({ - serverIp: "", + serverIp: "127.0.0.1", projectName: appName, }); } diff --git a/packages/server/src/utils/docker/domain.ts b/packages/server/src/utils/docker/domain.ts index 8094f1df2a..e710c81fac 100644 --- a/packages/server/src/utils/docker/domain.ts +++ b/packages/server/src/utils/docker/domain.ts @@ -162,6 +162,9 @@ export const addDomainToCompose = async ( } for (const domain of domains) { + if (domain.enabled === false) { + continue; + } const { serviceName, https } = domain; if (!serviceName) { throw new Error(`Domain "${domain.host}" is missing a service name`); diff --git a/packages/server/src/utils/traefik/domain.ts b/packages/server/src/utils/traefik/domain.ts index 596758b332..c3030461f5 100644 --- a/packages/server/src/utils/traefik/domain.ts +++ b/packages/server/src/utils/traefik/domain.ts @@ -13,6 +13,10 @@ import type { FileConfig, HttpRouter } from "./file-types"; import { createPathMiddlewares, removePathMiddlewares } from "./middleware"; export const manageDomain = async (app: ApplicationNested, domain: Domain) => { + if (domain.enabled === false) { + await removeDomain(app, domain.uniqueConfigKey); + return; + } const { appName } = app; let config: FileConfig;