Skip to content

Commit 5cd7de8

Browse files
authored
Merge pull request #3211 from Dokploy/canary
🚀 Release v0.26.1
2 parents 1352b85 + 85632fd commit 5cd7de8

31 files changed

Lines changed: 14457 additions & 99 deletions

File tree

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import type { Registry } from "@dokploy/server";
2+
import { getRegistryTag } from "@dokploy/server";
3+
import { describe, expect, it } from "vitest";
4+
5+
describe("getRegistryTag", () => {
6+
// Helper to create a mock registry
7+
const createMockRegistry = (overrides: Partial<Registry> = {}): Registry => {
8+
return {
9+
registryId: "test-registry-id",
10+
registryName: "Test Registry",
11+
username: "myuser",
12+
password: "test-password",
13+
registryUrl: "docker.io",
14+
registryType: "cloud",
15+
imagePrefix: null,
16+
createdAt: new Date().toISOString(),
17+
organizationId: "test-org-id",
18+
...overrides,
19+
};
20+
};
21+
22+
describe("with username (no imagePrefix)", () => {
23+
it("should handle simple image name without tag", () => {
24+
const registry = createMockRegistry({ username: "myuser" });
25+
const result = getRegistryTag(registry, "nginx");
26+
expect(result).toBe("docker.io/myuser/nginx");
27+
});
28+
29+
it("should handle image name with tag", () => {
30+
const registry = createMockRegistry({ username: "myuser" });
31+
const result = getRegistryTag(registry, "nginx:latest");
32+
expect(result).toBe("docker.io/myuser/nginx:latest");
33+
});
34+
35+
it("should handle image name with username already present (no duplication)", () => {
36+
const registry = createMockRegistry({ username: "myuser" });
37+
const result = getRegistryTag(registry, "myuser/myprivaterepo");
38+
// Should not duplicate username
39+
expect(result).toBe("docker.io/myuser/myprivaterepo");
40+
});
41+
42+
it("should handle image name with username and tag already present", () => {
43+
const registry = createMockRegistry({ username: "myuser" });
44+
const result = getRegistryTag(registry, "myuser/myprivaterepo:latest");
45+
// Should not duplicate username
46+
expect(result).toBe("docker.io/myuser/myprivaterepo:latest");
47+
});
48+
49+
it("should handle complex image name with username", () => {
50+
const registry = createMockRegistry({ username: "siumauricio" });
51+
const result = getRegistryTag(
52+
registry,
53+
"siumauricio/app-parse-multi-byte-port-e32uh7",
54+
);
55+
// Should not duplicate username
56+
expect(result).toBe(
57+
"docker.io/siumauricio/app-parse-multi-byte-port-e32uh7",
58+
);
59+
});
60+
61+
it("should handle image name with different username (should not duplicate)", () => {
62+
const registry = createMockRegistry({ username: "myuser" });
63+
const result = getRegistryTag(registry, "otheruser/myprivaterepo");
64+
expect(result).toBe("docker.io/myuser/myprivaterepo");
65+
});
66+
67+
it("should handle image name with full registry URL (no username)", () => {
68+
const registry = createMockRegistry({ username: "myuser" });
69+
const result = getRegistryTag(registry, "docker.io/nginx");
70+
// Should add username since imageName doesn't have one
71+
expect(result).toBe("docker.io/myuser/nginx");
72+
});
73+
74+
it("should handle image name with custom registry URL and username", () => {
75+
const registry = createMockRegistry({ username: "myuser" });
76+
const result = getRegistryTag(registry, "ghcr.io/myuser/repo");
77+
// Should not duplicate username even if registry URL is different
78+
expect(result).toBe("docker.io/myuser/repo");
79+
});
80+
81+
it("should handle image name with custom registry URL (different username)", () => {
82+
const registry = createMockRegistry({ username: "myuser" });
83+
const result = getRegistryTag(registry, "ghcr.io/otheruser/repo");
84+
// Should use registry username, not the one in imageName
85+
expect(result).toBe("docker.io/myuser/repo");
86+
});
87+
});
88+
89+
describe("with imagePrefix", () => {
90+
it("should use imagePrefix instead of username", () => {
91+
const registry = createMockRegistry({
92+
username: "myuser",
93+
imagePrefix: "myorg",
94+
});
95+
const result = getRegistryTag(registry, "nginx");
96+
expect(result).toBe("docker.io/myorg/nginx");
97+
});
98+
99+
it("should use imagePrefix with image tag", () => {
100+
const registry = createMockRegistry({
101+
username: "myuser",
102+
imagePrefix: "myorg",
103+
});
104+
const result = getRegistryTag(registry, "nginx:latest");
105+
expect(result).toBe("docker.io/myorg/nginx:latest");
106+
});
107+
108+
it("should handle imagePrefix with username already in image name", () => {
109+
const registry = createMockRegistry({
110+
username: "myuser",
111+
imagePrefix: "myorg",
112+
});
113+
const result = getRegistryTag(registry, "myuser/myprivaterepo");
114+
expect(result).toBe("docker.io/myorg/myprivaterepo");
115+
});
116+
117+
it("should handle imagePrefix matching image name prefix", () => {
118+
const registry = createMockRegistry({
119+
username: "myuser",
120+
imagePrefix: "myorg",
121+
});
122+
const result = getRegistryTag(registry, "myorg/myprivaterepo");
123+
// Should not duplicate prefix
124+
expect(result).toBe("docker.io/myorg/myprivaterepo");
125+
});
126+
});
127+
128+
describe("without registryUrl", () => {
129+
it("should work without registryUrl", () => {
130+
const registry = createMockRegistry({
131+
username: "myuser",
132+
registryUrl: "",
133+
});
134+
const result = getRegistryTag(registry, "nginx");
135+
expect(result).toBe("myuser/nginx");
136+
});
137+
138+
it("should work without registryUrl with imagePrefix", () => {
139+
const registry = createMockRegistry({
140+
username: "myuser",
141+
imagePrefix: "myorg",
142+
registryUrl: "",
143+
});
144+
const result = getRegistryTag(registry, "nginx");
145+
expect(result).toBe("myorg/nginx");
146+
});
147+
148+
it("should handle username already present without registryUrl", () => {
149+
const registry = createMockRegistry({
150+
username: "myuser",
151+
registryUrl: "",
152+
});
153+
const result = getRegistryTag(registry, "myuser/myprivaterepo");
154+
// Should not duplicate username
155+
expect(result).toBe("myuser/myprivaterepo");
156+
});
157+
});
158+
159+
describe("with custom registryUrl", () => {
160+
it("should handle custom registry URL", () => {
161+
const registry = createMockRegistry({
162+
username: "myuser",
163+
registryUrl: "ghcr.io",
164+
});
165+
const result = getRegistryTag(registry, "nginx");
166+
expect(result).toBe("ghcr.io/myuser/nginx");
167+
});
168+
169+
it("should handle custom registry URL with imagePrefix", () => {
170+
const registry = createMockRegistry({
171+
username: "myuser",
172+
imagePrefix: "myorg",
173+
registryUrl: "ghcr.io",
174+
});
175+
const result = getRegistryTag(registry, "nginx");
176+
expect(result).toBe("ghcr.io/myorg/nginx");
177+
});
178+
179+
it("should handle custom registry URL with username already present", () => {
180+
const registry = createMockRegistry({
181+
username: "myuser",
182+
registryUrl: "ghcr.io",
183+
});
184+
const result = getRegistryTag(registry, "myuser/myprivaterepo");
185+
// Should not duplicate username
186+
expect(result).toBe("ghcr.io/myuser/myprivaterepo");
187+
});
188+
});
189+
190+
describe("edge cases", () => {
191+
it("should handle empty image name", () => {
192+
const registry = createMockRegistry({ username: "myuser" });
193+
const result = getRegistryTag(registry, "");
194+
expect(result).toBe("docker.io/myuser/");
195+
});
196+
197+
it("should handle image name with multiple slashes", () => {
198+
const registry = createMockRegistry({ username: "myuser" });
199+
const result = getRegistryTag(registry, "org/suborg/repo");
200+
expect(result).toBe("docker.io/myuser/repo");
201+
});
202+
203+
it("should handle image name with username at different position", () => {
204+
const registry = createMockRegistry({ username: "myuser" });
205+
const result = getRegistryTag(registry, "org/myuser/repo");
206+
expect(result).toBe("docker.io/myuser/repo");
207+
});
208+
});
209+
});

apps/dokploy/__test__/drop/drop.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const baseApp: ApplicationNested = {
2828
railpackVersion: "0.2.2",
2929
applicationId: "",
3030
previewLabels: [],
31+
createEnvFile: true,
3132
herokuVersion: "",
3233
giteaBranch: "",
3334
buildServerId: "",
@@ -67,6 +68,7 @@ const baseApp: ApplicationNested = {
6768
previewWildcard: "",
6869
environment: {
6970
env: "",
71+
isDefault: false,
7072
environmentId: "",
7173
name: "",
7274
createdAt: "",

apps/dokploy/__test__/requests/request.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,22 @@ describe("processLogs", () => {
5454
const result = parseRawConfig(entryWithWhitespace);
5555
expect(result.data).toHaveLength(2);
5656
});
57+
58+
it("should filter out Dokploy dashboard requests", () => {
59+
const dokployDashboardEntry = `{"ClientAddr":"172.71.187.131:9485","ClientHost":"172.71.187.131","ClientPort":"9485","ClientUsername":"-","DownstreamContentSize":14550,"DownstreamStatus":200,"Duration":57681682,"OriginContentSize":14550,"OriginDuration":57612242,"OriginStatus":200,"Overhead":69440,"RequestAddr":"hostinger.dokploy.com","RequestContentSize":0,"RequestCount":20142,"RequestHost":"hostinger.dokploy.com","RequestMethod":"GET","RequestPath":"/_next/data/cb_zzI4Rp9G7Q7djrFKh0/en/dashboard/traefik.json","RequestPort":"-","RequestProtocol":"HTTP/2.0","RequestScheme":"https","RetryAttempts":0,"RouterName":"dokploy-router-app-secure@file","ServiceAddr":"dokploy:3000","ServiceName":"dokploy-service-app@file","ServiceURL":"http://dokploy:3000","StartLocal":"2025-12-10T05:10:41.957755949Z","StartUTC":"2025-12-10T05:10:41.957755949Z","TLSCipher":"TLS_AES_128_GCM_SHA256","TLSVersion":"1.3","entryPointName":"websecure","level":"info","msg":"","time":"2025-12-10T05:10:42Z"}`;
60+
61+
// Test with only Dokploy dashboard entry - should be filtered out
62+
const resultOnlyDokploy = parseRawConfig(dokployDashboardEntry);
63+
expect(resultOnlyDokploy.data).toHaveLength(0);
64+
expect(resultOnlyDokploy.totalCount).toBe(0);
65+
66+
// Test with mixed entries - Dokploy should be filtered, others should remain
67+
const mixedEntries = `${dokployDashboardEntry}\n${sampleLogEntry}`;
68+
const resultMixed = parseRawConfig(mixedEntries);
69+
expect(resultMixed.data).toHaveLength(1);
70+
expect(resultMixed.totalCount).toBe(1);
71+
expect(resultMixed.data[0]?.ServiceName).not.toBe(
72+
"dokploy-service-app@file",
73+
);
74+
});
5775
});

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const baseApp: ApplicationNested = {
77
rollbackActive: false,
88
applicationId: "",
99
previewLabels: [],
10+
createEnvFile: true,
1011
herokuVersion: "",
1112
giteaRepository: "",
1213
giteaOwner: "",
@@ -49,6 +50,7 @@ const baseApp: ApplicationNested = {
4950
environmentId: "",
5051
environment: {
5152
env: "",
53+
isDefault: false,
5254
environmentId: "",
5355
name: "",
5456
createdAt: "",

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,8 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
208208
const certificateType = form.watch("certificateType");
209209
const https = form.watch("https");
210210
const domainType = form.watch("domainType");
211+
const host = form.watch("host");
212+
const isTraefikMeDomain = host?.includes("traefik.me") || false;
211213

212214
useEffect(() => {
213215
if (data) {
@@ -502,6 +504,13 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
502504
to make your traefik.me domain work.
503505
</AlertBlock>
504506
)}
507+
{isTraefikMeDomain && (
508+
<AlertBlock type="info">
509+
<strong>Note:</strong> traefik.me is a public HTTP
510+
service and does not support SSL/HTTPS. HTTPS and
511+
certificate options will not have any effect.
512+
</AlertBlock>
513+
)}
505514
<FormLabel>Host</FormLabel>
506515
<div className="flex gap-2">
507516
<FormControl>

apps/dokploy/components/dashboard/application/environment/show.tsx

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,23 @@ import { toast } from "sonner";
55
import { z } from "zod";
66
import { Button } from "@/components/ui/button";
77
import { Card } from "@/components/ui/card";
8-
import { Form } from "@/components/ui/form";
8+
import {
9+
Form,
10+
FormControl,
11+
FormDescription,
12+
FormField,
13+
FormItem,
14+
FormLabel,
15+
} from "@/components/ui/form";
916
import { Secrets } from "@/components/ui/secrets";
17+
import { Switch } from "@/components/ui/switch";
1018
import { api } from "@/utils/api";
1119

1220
const addEnvironmentSchema = z.object({
1321
env: z.string(),
1422
buildArgs: z.string(),
1523
buildSecrets: z.string(),
24+
createEnvFile: z.boolean(),
1625
});
1726

1827
type EnvironmentSchema = z.infer<typeof addEnvironmentSchema>;
@@ -39,6 +48,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
3948
env: "",
4049
buildArgs: "",
4150
buildSecrets: "",
51+
createEnvFile: true,
4252
},
4353
resolver: zodResolver(addEnvironmentSchema),
4454
});
@@ -47,17 +57,20 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
4757
const currentEnv = form.watch("env");
4858
const currentBuildArgs = form.watch("buildArgs");
4959
const currentBuildSecrets = form.watch("buildSecrets");
60+
const currentCreateEnvFile = form.watch("createEnvFile");
5061
const hasChanges =
5162
currentEnv !== (data?.env || "") ||
5263
currentBuildArgs !== (data?.buildArgs || "") ||
53-
currentBuildSecrets !== (data?.buildSecrets || "");
64+
currentBuildSecrets !== (data?.buildSecrets || "") ||
65+
currentCreateEnvFile !== (data?.createEnvFile ?? true);
5466

5567
useEffect(() => {
5668
if (data) {
5769
form.reset({
5870
env: data.env || "",
5971
buildArgs: data.buildArgs || "",
6072
buildSecrets: data.buildSecrets || "",
73+
createEnvFile: data.createEnvFile ?? true,
6174
});
6275
}
6376
}, [data, form]);
@@ -67,6 +80,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
6780
env: formData.env,
6881
buildArgs: formData.buildArgs,
6982
buildSecrets: formData.buildSecrets,
83+
createEnvFile: formData.createEnvFile,
7084
applicationId,
7185
})
7286
.then(async () => {
@@ -83,6 +97,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
8397
env: data?.env || "",
8498
buildArgs: data?.buildArgs || "",
8599
buildSecrets: data?.buildSecrets || "",
100+
createEnvFile: data?.createEnvFile ?? true,
86101
});
87102
};
88103

@@ -167,6 +182,30 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
167182
placeholder="NPM_TOKEN=xyz"
168183
/>
169184
)}
185+
{data?.buildType === "dockerfile" && (
186+
<FormField
187+
control={form.control}
188+
name="createEnvFile"
189+
render={({ field }) => (
190+
<FormItem className="flex flex-row items-center justify-between p-3 border rounded-lg shadow-sm">
191+
<div className="space-y-0.5">
192+
<FormLabel>Create Environment File</FormLabel>
193+
<FormDescription>
194+
When enabled, an .env file will be created during the
195+
build process. Disable this if you don't want to generate
196+
an environment file.
197+
</FormDescription>
198+
</div>
199+
<FormControl>
200+
<Switch
201+
checked={field.value}
202+
onCheckedChange={field.onChange}
203+
/>
204+
</FormControl>
205+
</FormItem>
206+
)}
207+
/>
208+
)}
170209
<div className="flex flex-row justify-end gap-2">
171210
{hasChanges && (
172211
<Button type="button" variant="outline" onClick={handleCancel}>

0 commit comments

Comments
 (0)