Skip to content

Commit 3f1bf2b

Browse files
authored
Merge pull request #2863 from KarpachMarko/feature/custom-entrypoint
feat: add support for custom entry point
2 parents c7814bb + 2683ac2 commit 3f1bf2b

File tree

8 files changed

+326
-12
lines changed

8 files changed

+326
-12
lines changed

apps/dokploy/__test__/compose/domain/host-rule-format.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ describe("Host rule format regression tests", () => {
3232
previewDeploymentId: "",
3333
internalPath: "/",
3434
stripPath: false,
35+
customEntrypoint: null,
3536
};
3637

3738
describe("Host rule format validation", () => {

apps/dokploy/__test__/compose/domain/labels.test.ts

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ describe("createDomainLabels", () => {
77
const baseDomain: Domain = {
88
host: "example.com",
99
port: 8080,
10+
customEntrypoint: null,
1011
https: false,
1112
uniqueConfigKey: 1,
1213
customCertResolver: null,
@@ -240,4 +241,134 @@ describe("createDomainLabels", () => {
240241
"traefik.http.routers.test-app-1-websecure.middlewares=stripprefix-test-app-1,addprefix-test-app-1",
241242
);
242243
});
244+
245+
it("should create basic labels for custom entrypoint", async () => {
246+
const labels = await createDomainLabels(
247+
appName,
248+
{ ...baseDomain, customEntrypoint: "custom" },
249+
"custom",
250+
);
251+
expect(labels).toEqual([
252+
"traefik.http.routers.test-app-1-custom.rule=Host(`example.com`)",
253+
"traefik.http.routers.test-app-1-custom.entrypoints=custom",
254+
"traefik.http.services.test-app-1-custom.loadbalancer.server.port=8080",
255+
"traefik.http.routers.test-app-1-custom.service=test-app-1-custom",
256+
]);
257+
});
258+
259+
it("should create https labels for custom entrypoint", async () => {
260+
const labels = await createDomainLabels(
261+
appName,
262+
{
263+
...baseDomain,
264+
https: true,
265+
customEntrypoint: "custom",
266+
certificateType: "letsencrypt",
267+
},
268+
"custom",
269+
);
270+
expect(labels).toEqual([
271+
"traefik.http.routers.test-app-1-custom.rule=Host(`example.com`)",
272+
"traefik.http.routers.test-app-1-custom.entrypoints=custom",
273+
"traefik.http.services.test-app-1-custom.loadbalancer.server.port=8080",
274+
"traefik.http.routers.test-app-1-custom.service=test-app-1-custom",
275+
"traefik.http.routers.test-app-1-custom.tls.certresolver=letsencrypt",
276+
]);
277+
});
278+
279+
it("should add stripPath middleware for custom entrypoint", async () => {
280+
const labels = await createDomainLabels(
281+
appName,
282+
{
283+
...baseDomain,
284+
customEntrypoint: "custom",
285+
path: "/api",
286+
stripPath: true,
287+
},
288+
"custom",
289+
);
290+
291+
expect(labels).toContain(
292+
"traefik.http.middlewares.stripprefix-test-app-1.stripprefix.prefixes=/api",
293+
);
294+
expect(labels).toContain(
295+
"traefik.http.routers.test-app-1-custom.middlewares=stripprefix-test-app-1",
296+
);
297+
});
298+
299+
it("should add internalPath middleware for custom entrypoint", async () => {
300+
const labels = await createDomainLabels(
301+
appName,
302+
{
303+
...baseDomain,
304+
customEntrypoint: "custom",
305+
internalPath: "/hello",
306+
},
307+
"custom",
308+
);
309+
310+
expect(labels).toContain(
311+
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
312+
);
313+
expect(labels).toContain(
314+
"traefik.http.routers.test-app-1-custom.middlewares=addprefix-test-app-1",
315+
);
316+
});
317+
318+
it("should add path prefix in rule for custom entrypoint", async () => {
319+
const labels = await createDomainLabels(
320+
appName,
321+
{
322+
...baseDomain,
323+
customEntrypoint: "custom",
324+
path: "/api",
325+
},
326+
"custom",
327+
);
328+
329+
expect(labels).toContain(
330+
"traefik.http.routers.test-app-1-custom.rule=Host(`example.com`) && PathPrefix(`/api`)",
331+
);
332+
});
333+
334+
it("should combine all middlewares for custom entrypoint", async () => {
335+
const labels = await createDomainLabels(
336+
appName,
337+
{
338+
...baseDomain,
339+
customEntrypoint: "custom",
340+
path: "/api",
341+
stripPath: true,
342+
internalPath: "/hello",
343+
},
344+
"custom",
345+
);
346+
347+
expect(labels).toContain(
348+
"traefik.http.middlewares.stripprefix-test-app-1.stripprefix.prefixes=/api",
349+
);
350+
expect(labels).toContain(
351+
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
352+
);
353+
expect(labels).toContain(
354+
"traefik.http.routers.test-app-1-custom.middlewares=stripprefix-test-app-1,addprefix-test-app-1",
355+
);
356+
});
357+
358+
it("should not add redirect-to-https for custom entrypoint even with https", async () => {
359+
const labels = await createDomainLabels(
360+
appName,
361+
{
362+
...baseDomain,
363+
customEntrypoint: "custom",
364+
https: true,
365+
certificateType: "letsencrypt",
366+
},
367+
"custom",
368+
);
369+
370+
const middlewareLabel = labels.find((l) => l.includes(".middlewares="));
371+
// Should not contain redirect-to-https since there's only one router
372+
expect(middlewareLabel).toBeUndefined();
373+
});
243374
});

apps/dokploy/__test__/traefik/traefik.test.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ const baseDomain: Domain = {
137137
https: false,
138138
path: null,
139139
port: null,
140+
customEntrypoint: null,
140141
serviceName: "",
141142
composeId: "",
142143
customCertResolver: null,
@@ -276,6 +277,110 @@ test("CertificateType on websecure entrypoint", async () => {
276277
expect(router.tls?.certResolver).toBe("letsencrypt");
277278
});
278279

280+
test("Custom entrypoint on http domain", async () => {
281+
const router = await createRouterConfig(
282+
baseApp,
283+
{ ...baseDomain, https: false, customEntrypoint: "custom" },
284+
"custom",
285+
);
286+
287+
expect(router.entryPoints).toEqual(["custom"]);
288+
expect(router.middlewares).not.toContain("redirect-to-https");
289+
expect(router.tls).toBeUndefined();
290+
});
291+
292+
test("Custom entrypoint on https domain", async () => {
293+
const router = await createRouterConfig(
294+
baseApp,
295+
{
296+
...baseDomain,
297+
https: true,
298+
customEntrypoint: "custom",
299+
certificateType: "letsencrypt",
300+
},
301+
"custom",
302+
);
303+
304+
expect(router.entryPoints).toEqual(["custom"]);
305+
expect(router.middlewares).not.toContain("redirect-to-https");
306+
expect(router.tls?.certResolver).toBe("letsencrypt");
307+
});
308+
309+
test("Custom entrypoint with path includes PathPrefix in rule", async () => {
310+
const router = await createRouterConfig(
311+
baseApp,
312+
{ ...baseDomain, customEntrypoint: "custom", path: "/api" },
313+
"custom",
314+
);
315+
316+
expect(router.rule).toContain("PathPrefix(`/api`)");
317+
expect(router.entryPoints).toEqual(["custom"]);
318+
});
319+
320+
test("Custom entrypoint with stripPath adds stripprefix middleware", async () => {
321+
const router = await createRouterConfig(
322+
baseApp,
323+
{
324+
...baseDomain,
325+
customEntrypoint: "custom",
326+
path: "/api",
327+
stripPath: true,
328+
},
329+
"custom",
330+
);
331+
332+
expect(router.middlewares).toContain("stripprefix--1");
333+
expect(router.entryPoints).toEqual(["custom"]);
334+
});
335+
336+
test("Custom entrypoint with internalPath adds addprefix middleware", async () => {
337+
const router = await createRouterConfig(
338+
baseApp,
339+
{
340+
...baseDomain,
341+
customEntrypoint: "custom",
342+
internalPath: "/hello",
343+
},
344+
"custom",
345+
);
346+
347+
expect(router.middlewares).toContain("addprefix--1");
348+
expect(router.entryPoints).toEqual(["custom"]);
349+
});
350+
351+
test("Custom entrypoint with https and custom cert resolver", async () => {
352+
const router = await createRouterConfig(
353+
baseApp,
354+
{
355+
...baseDomain,
356+
https: true,
357+
customEntrypoint: "custom",
358+
certificateType: "custom",
359+
customCertResolver: "myresolver",
360+
},
361+
"custom",
362+
);
363+
364+
expect(router.entryPoints).toEqual(["custom"]);
365+
expect(router.tls?.certResolver).toBe("myresolver");
366+
});
367+
368+
test("Custom entrypoint without https should not have tls", async () => {
369+
const router = await createRouterConfig(
370+
baseApp,
371+
{
372+
...baseDomain,
373+
https: false,
374+
customEntrypoint: "custom",
375+
certificateType: "letsencrypt",
376+
},
377+
"custom",
378+
);
379+
380+
expect(router.entryPoints).toEqual(["custom"]);
381+
expect(router.tls).toBeUndefined();
382+
});
383+
279384
/** IDN/Punycode */
280385

281386
test("Internationalized domain name is converted to punycode", async () => {

apps/dokploy/components/dashboard/application/domains/handle-domain.tsx

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ export const domain = z
6161
.min(1, { message: "Port must be at least 1" })
6262
.max(65535, { message: "Port must be 65535 or below" })
6363
.optional(),
64+
useCustomEntrypoint: z.boolean(),
65+
customEntrypoint: z.string().optional(),
6466
https: z.boolean().optional(),
6567
certificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
6668
customCertResolver: z.string().optional(),
@@ -114,6 +116,14 @@ export const domain = z
114116
message: "Internal path must start with '/'",
115117
});
116118
}
119+
120+
if (input.useCustomEntrypoint && !input.customEntrypoint) {
121+
ctx.addIssue({
122+
code: z.ZodIssueCode.custom,
123+
path: ["customEntrypoint"],
124+
message: "Custom entry point must be specified",
125+
});
126+
}
117127
});
118128

119129
type Domain = z.infer<typeof domain>;
@@ -196,6 +206,8 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
196206
internalPath: undefined,
197207
stripPath: false,
198208
port: undefined,
209+
useCustomEntrypoint: false,
210+
customEntrypoint: undefined,
199211
https: false,
200212
certificateType: undefined,
201213
customCertResolver: undefined,
@@ -206,6 +218,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
206218
});
207219

208220
const certificateType = form.watch("certificateType");
221+
const useCustomEntrypoint = form.watch("useCustomEntrypoint");
209222
const https = form.watch("https");
210223
const domainType = form.watch("domainType");
211224
const host = form.watch("host");
@@ -220,6 +233,8 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
220233
internalPath: data?.internalPath || undefined,
221234
stripPath: data?.stripPath || false,
222235
port: data?.port || undefined,
236+
useCustomEntrypoint: !!data.customEntrypoint,
237+
customEntrypoint: data.customEntrypoint || undefined,
223238
certificateType: data?.certificateType || undefined,
224239
customCertResolver: data?.customCertResolver || undefined,
225240
serviceName: data?.serviceName || undefined,
@@ -234,6 +249,8 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
234249
internalPath: undefined,
235250
stripPath: false,
236251
port: undefined,
252+
useCustomEntrypoint: false,
253+
customEntrypoint: undefined,
237254
https: false,
238255
certificateType: undefined,
239256
customCertResolver: undefined,
@@ -635,6 +652,50 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
635652
}}
636653
/>
637654

655+
<FormField
656+
control={form.control}
657+
name="useCustomEntrypoint"
658+
render={({ field }) => (
659+
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
660+
<div className="space-y-0.5">
661+
<FormLabel>Custom Entrypoint</FormLabel>
662+
<FormDescription>
663+
Use custom entrypoint for domina
664+
<br />
665+
"web" and/or "websecure" is used by default.
666+
</FormDescription>
667+
<FormMessage />
668+
</div>
669+
<FormControl>
670+
<Switch
671+
checked={field.value}
672+
onCheckedChange={field.onChange}
673+
/>
674+
</FormControl>
675+
</FormItem>
676+
)}
677+
/>
678+
679+
{useCustomEntrypoint && (
680+
<FormField
681+
control={form.control}
682+
name="customEntrypoint"
683+
render={({ field }) => (
684+
<FormItem className="w-full">
685+
<FormLabel>Entrypoint Name</FormLabel>
686+
<FormControl>
687+
<Input
688+
placeholder="Enter entrypoint name manually"
689+
{...field}
690+
className="w-full"
691+
/>
692+
</FormControl>
693+
<FormMessage />
694+
</FormItem>
695+
)}
696+
/>
697+
)}
698+
638699
<FormField
639700
control={form.control}
640701
name="https"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE "domain" ADD COLUMN "customEntrypoint" text;

0 commit comments

Comments
 (0)