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 27e696b207..a26fbc7fb4 100644 --- a/apps/dokploy/__test__/compose/domain/host-rule-format.test.ts +++ b/apps/dokploy/__test__/compose/domain/host-rule-format.test.ts @@ -32,6 +32,7 @@ describe("Host rule format regression tests", () => { previewDeploymentId: "", internalPath: "/", stripPath: false, + customEntrypoint: null, }; describe("Host rule format validation", () => { diff --git a/apps/dokploy/__test__/compose/domain/labels.test.ts b/apps/dokploy/__test__/compose/domain/labels.test.ts index 9a75e0a845..61afec36c7 100644 --- a/apps/dokploy/__test__/compose/domain/labels.test.ts +++ b/apps/dokploy/__test__/compose/domain/labels.test.ts @@ -7,6 +7,7 @@ describe("createDomainLabels", () => { const baseDomain: Domain = { host: "example.com", port: 8080, + customEntrypoint: null, https: false, uniqueConfigKey: 1, customCertResolver: null, @@ -240,4 +241,38 @@ describe("createDomainLabels", () => { "traefik.http.routers.test-app-1-websecure.middlewares=stripprefix-test-app-1,addprefix-test-app-1", ); }); + + it("should create basic labels for custom entrypoint", async () => { + const labels = await createDomainLabels( + appName, + { ...baseDomain, customEntrypoint: "custom" }, + "custom", + ); + expect(labels).toEqual([ + "traefik.http.routers.test-app-1-custom.rule=Host(`example.com`)", + "traefik.http.routers.test-app-1-custom.entrypoints=custom", + "traefik.http.services.test-app-1-custom.loadbalancer.server.port=8080", + "traefik.http.routers.test-app-1-custom.service=test-app-1-custom", + ]); + }); + + it("should create https labels for custom entrypoint", async () => { + const labels = await createDomainLabels( + appName, + { + ...baseDomain, + https: true, + customEntrypoint: "custom", + certificateType: "letsencrypt", + }, + "custom", + ); + expect(labels).toEqual([ + "traefik.http.routers.test-app-1-custom.rule=Host(`example.com`)", + "traefik.http.routers.test-app-1-custom.entrypoints=custom", + "traefik.http.services.test-app-1-custom.loadbalancer.server.port=8080", + "traefik.http.routers.test-app-1-custom.service=test-app-1-custom", + "traefik.http.routers.test-app-1-custom.tls.certresolver=letsencrypt", + ]); + }); }); diff --git a/apps/dokploy/__test__/traefik/traefik.test.ts b/apps/dokploy/__test__/traefik/traefik.test.ts index 8e678413ce..218b9ba1e1 100644 --- a/apps/dokploy/__test__/traefik/traefik.test.ts +++ b/apps/dokploy/__test__/traefik/traefik.test.ts @@ -135,6 +135,7 @@ const baseDomain: Domain = { https: false, path: null, port: null, + customEntrypoint: null, serviceName: "", composeId: "", customCertResolver: null, @@ -273,3 +274,32 @@ test("CertificateType on websecure entrypoint", async () => { expect(router.tls?.certResolver).toBe("letsencrypt"); }); + +test("Custom entrypoint on http domain", async () => { + const router = await createRouterConfig( + baseApp, + { ...baseDomain, https: false, customEntrypoint: "custom" }, + "custom", + ); + + expect(router.entryPoints).toEqual(["custom"]); + expect(router.middlewares).not.toContain("redirect-to-https"); + expect(router.tls).toBeUndefined(); +}); + +test("Custom entrypoint on https domain", async () => { + const router = await createRouterConfig( + baseApp, + { + ...baseDomain, + https: true, + customEntrypoint: "custom", + certificateType: "letsencrypt", + }, + "custom", + ); + + expect(router.entryPoints).toEqual(["custom"]); + expect(router.middlewares).not.toContain("redirect-to-https"); + expect(router.tls?.certResolver).toBe("letsencrypt"); +}); diff --git a/apps/dokploy/components/dashboard/application/domains/handle-domain.tsx b/apps/dokploy/components/dashboard/application/domains/handle-domain.tsx index 6af0e1e8c9..a37f4959b3 100644 --- a/apps/dokploy/components/dashboard/application/domains/handle-domain.tsx +++ b/apps/dokploy/components/dashboard/application/domains/handle-domain.tsx @@ -61,6 +61,8 @@ export const domain = z .min(1, { message: "Port must be at least 1" }) .max(65535, { message: "Port must be 65535 or below" }) .optional(), + useCustomEntrypoint: z.boolean(), + customEntrypoint: z.string().optional(), https: z.boolean().optional(), certificateType: z.enum(["letsencrypt", "none", "custom"]).optional(), customCertResolver: z.string().optional(), @@ -114,6 +116,14 @@ export const domain = z message: "Internal path must start with '/'", }); } + + if (input.useCustomEntrypoint && !input.customEntrypoint) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["customEntrypoint"], + message: "Custom entry point must be specified", + }); + } }); type Domain = z.infer; @@ -196,6 +206,8 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => { internalPath: undefined, stripPath: false, port: undefined, + useCustomEntrypoint: false, + customEntrypoint: undefined, https: false, certificateType: undefined, customCertResolver: undefined, @@ -206,6 +218,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => { }); const certificateType = form.watch("certificateType"); + const useCustomEntrypoint = form.watch("useCustomEntrypoint"); const https = form.watch("https"); const domainType = form.watch("domainType"); const host = form.watch("host"); @@ -220,6 +233,8 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => { internalPath: data?.internalPath || undefined, stripPath: data?.stripPath || false, port: data?.port || undefined, + useCustomEntrypoint: !!data.customEntrypoint, + customEntrypoint: data.customEntrypoint || undefined, certificateType: data?.certificateType || undefined, customCertResolver: data?.customCertResolver || undefined, serviceName: data?.serviceName || undefined, @@ -234,6 +249,8 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => { internalPath: undefined, stripPath: false, port: undefined, + useCustomEntrypoint: false, + customEntrypoint: undefined, https: false, certificateType: undefined, customCertResolver: undefined, @@ -635,6 +652,50 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => { }} /> + ( + +
+ Custom Entrypoint + + Use custom entrypoint for domina +
+ "web" and/or "websecure" is used by default. +
+ +
+ + + +
+ )} + /> + + {useCustomEntrypoint && ( + ( + + Entrypoint Name + + + + + + )} + /> + )} + { const { host, port, + customEntrypoint, https, uniqueConfigKey, certificateType, @@ -282,7 +287,7 @@ export const createDomainLabels = ( if (stripPath && path && path !== "/") { const middlewareName = `stripprefix-${appName}-${uniqueConfigKey}`; // Only define middleware once (on web entrypoint) - if (entrypoint === "web") { + if (entrypoint === "web" || customEntrypoint != null) { labels.push( `traefik.http.middlewares.${middlewareName}.stripprefix.prefixes=${path}`, ); @@ -294,7 +299,7 @@ export const createDomainLabels = ( if (internalPath && internalPath !== "/" && internalPath.startsWith("/")) { const middlewareName = `addprefix-${appName}-${uniqueConfigKey}`; // Only define middleware once (on web entrypoint) - if (entrypoint === "web") { + if (entrypoint === "web" || customEntrypoint != null) { labels.push( `traefik.http.middlewares.${middlewareName}.addprefix.prefix=${internalPath}`, ); @@ -310,7 +315,7 @@ export const createDomainLabels = ( } // Add TLS configuration for websecure - if (entrypoint === "websecure") { + if (entrypoint === "websecure" || (customEntrypoint != null && https)) { if (certificateType === "letsencrypt") { labels.push( `traefik.http.routers.${routerName}.tls.certresolver=letsencrypt`, diff --git a/packages/server/src/utils/traefik/domain.ts b/packages/server/src/utils/traefik/domain.ts index 68095fa80f..7f8edd324f 100644 --- a/packages/server/src/utils/traefik/domain.ts +++ b/packages/server/src/utils/traefik/domain.ts @@ -32,10 +32,10 @@ export const manageDomain = async (app: ApplicationNested, domain: Domain) => { config.http.routers[routerName] = await createRouterConfig( app, domain, - "web", + domain.customEntrypoint || "web", ); - if (domain.https) { + if (domain.customEntrypoint == null && domain.https) { config.http.routers[routerNameSecure] = await createRouterConfig( app, domain, @@ -107,13 +107,20 @@ export const removeDomain = async ( export const createRouterConfig = async ( app: ApplicationNested, domain: Domain, - entryPoint: "web" | "websecure", + entryPoint: string, ) => { const { appName, redirects, security } = app; const { certificateType } = domain; - const { host, path, https, uniqueConfigKey, internalPath, stripPath } = - domain; + const { + host, + path, + https, + uniqueConfigKey, + internalPath, + stripPath, + customEntrypoint, + } = domain; const routerConfig: HttpRouter = { rule: `Host(\`${host}\`)${path !== null && path !== "/" ? ` && PathPrefix(\`${path}\`)` : ""}`, service: `${appName}-service-${uniqueConfigKey}`, @@ -162,7 +169,7 @@ export const createRouterConfig = async ( } } - if (entryPoint === "websecure") { + if (entryPoint === "websecure" || (customEntrypoint != null && https)) { if (certificateType === "letsencrypt") { routerConfig.tls = { certResolver: "letsencrypt" }; } else if (certificateType === "custom" && domain.customCertResolver) {