Merge pull request #3043 from Dokploy/canary

🚀 Release v0.25.7
This commit is contained in:
Mauricio Siu
2025-11-18 23:01:38 -06:00
committed by GitHub
90 changed files with 15672 additions and 3171 deletions
+3 -3
View File
@@ -6,9 +6,9 @@ Please describe in a short paragraph what this PR is about.
Before submitting this PR, please make sure that: Before submitting this PR, please make sure that:
- [] You created a dedicated branch based on the `canary` branch. - [ ] You created a dedicated branch based on the `canary` branch.
- [] You have read the suggestions in the CONTRIBUTING.md file https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#pull-request - [ ] You have read the suggestions in the CONTRIBUTING.md file https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#pull-request
- [] You have tested this PR in your local instance. - [ ] You have tested this PR in your local instance.
## Issues related (if applicable) ## Issues related (if applicable)
+10 -10
View File
@@ -1,9 +1,9 @@
import { import {
deployRemoteApplication, deployApplication,
deployRemoteCompose, deployCompose,
deployRemotePreviewApplication, deployPreviewApplication,
rebuildRemoteApplication, rebuildApplication,
rebuildRemoteCompose, rebuildCompose,
updateApplicationStatus, updateApplicationStatus,
updateCompose, updateCompose,
updatePreviewDeployment, updatePreviewDeployment,
@@ -16,13 +16,13 @@ export const deploy = async (job: DeployJob) => {
await updateApplicationStatus(job.applicationId, "running"); await updateApplicationStatus(job.applicationId, "running");
if (job.server) { if (job.server) {
if (job.type === "redeploy") { if (job.type === "redeploy") {
await rebuildRemoteApplication({ await rebuildApplication({
applicationId: job.applicationId, applicationId: job.applicationId,
titleLog: job.titleLog || "Rebuild deployment", titleLog: job.titleLog || "Rebuild deployment",
descriptionLog: job.descriptionLog || "", descriptionLog: job.descriptionLog || "",
}); });
} else if (job.type === "deploy") { } else if (job.type === "deploy") {
await deployRemoteApplication({ await deployApplication({
applicationId: job.applicationId, applicationId: job.applicationId,
titleLog: job.titleLog || "Manual deployment", titleLog: job.titleLog || "Manual deployment",
descriptionLog: job.descriptionLog || "", descriptionLog: job.descriptionLog || "",
@@ -36,13 +36,13 @@ export const deploy = async (job: DeployJob) => {
if (job.server) { if (job.server) {
if (job.type === "redeploy") { if (job.type === "redeploy") {
await rebuildRemoteCompose({ await rebuildCompose({
composeId: job.composeId, composeId: job.composeId,
titleLog: job.titleLog || "Rebuild deployment", titleLog: job.titleLog || "Rebuild deployment",
descriptionLog: job.descriptionLog || "", descriptionLog: job.descriptionLog || "",
}); });
} else if (job.type === "deploy") { } else if (job.type === "deploy") {
await deployRemoteCompose({ await deployCompose({
composeId: job.composeId, composeId: job.composeId,
titleLog: job.titleLog || "Manual deployment", titleLog: job.titleLog || "Manual deployment",
descriptionLog: job.descriptionLog || "", descriptionLog: job.descriptionLog || "",
@@ -55,7 +55,7 @@ export const deploy = async (job: DeployJob) => {
}); });
if (job.server) { if (job.server) {
if (job.type === "deploy") { if (job.type === "deploy") {
await deployRemotePreviewApplication({ await deployPreviewApplication({
applicationId: job.applicationId, applicationId: job.applicationId,
titleLog: job.titleLog || "Preview Deployment", titleLog: job.titleLog || "Preview Deployment",
descriptionLog: job.descriptionLog || "", descriptionLog: job.descriptionLog || "",
+311 -1
View File
@@ -1,5 +1,10 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { extractCommitMessage } from "@/pages/api/deploy/[refreshToken]"; import {
extractCommitMessage,
extractImageName,
extractImageTag,
extractImageTagFromRequest,
} from "@/pages/api/deploy/[refreshToken]";
describe("GitHub Webhook Skip CI", () => { describe("GitHub Webhook Skip CI", () => {
const mockGithubHeaders = { const mockGithubHeaders = {
@@ -96,3 +101,308 @@ describe("GitHub Webhook Skip CI", () => {
); );
}); });
}); });
describe("GitHub Packages Docker Image Tag Extraction", () => {
it("should extract tag from container_metadata", () => {
const headers = { "x-github-event": "registry_package" };
const body = {
registry_package: {
package_version: {
version: "sha256:abc123...",
container_metadata: {
tag: {
name: "v1.0.0",
digest: "sha256:abc123...",
},
},
package_url: "ghcr.io/owner/repo:v1.0.0",
},
},
};
const tag = extractImageTagFromRequest(headers, body);
expect(tag).toBe("v1.0.0");
});
it("should extract tag from package_url when container_metadata tag matches version", () => {
const headers = { "x-github-event": "registry_package" };
const body = {
registry_package: {
package_version: {
version: "sha256:abc123...",
container_metadata: {
tag: {
name: "sha256:abc123...",
digest: "sha256:abc123...",
},
},
package_url: "ghcr.io/owner/repo:latest",
},
},
};
const tag = extractImageTagFromRequest(headers, body);
expect(tag).toBe("latest");
});
it("should extract tag from package_url when container_metadata is missing", () => {
const headers = { "x-github-event": "registry_package" };
const body = {
registry_package: {
package_version: {
version: "sha256:abc123...",
package_url: "ghcr.io/owner/repo:1.2.3",
},
},
};
const tag = extractImageTagFromRequest(headers, body);
expect(tag).toBe("1.2.3");
});
it("should handle different tag formats in package_url", () => {
const headers = { "x-github-event": "registry_package" };
const testCases = [
{ url: "ghcr.io/owner/repo:latest", expected: "latest" },
{ url: "ghcr.io/owner/repo:v1.0.0", expected: "v1.0.0" },
{ url: "ghcr.io/owner/repo:1.2.3", expected: "1.2.3" },
{ url: "ghcr.io/owner/repo:dev", expected: "dev" },
];
for (const testCase of testCases) {
const body = {
registry_package: {
package_version: {
version: "sha256:abc123...",
package_url: testCase.url,
},
},
};
const tag = extractImageTagFromRequest(headers, body);
expect(tag).toBe(testCase.expected);
}
});
it("should return null for non-registry_package events", () => {
const headers = { "x-github-event": "push" };
const body = {
registry_package: {
package_version: {
package_url: "ghcr.io/owner/repo:latest",
},
},
};
const tag = extractImageTagFromRequest(headers, body);
expect(tag).toBeNull();
});
it("should return null when package_version is missing", () => {
const headers = { "x-github-event": "registry_package" };
const body = {
registry_package: {},
};
const tag = extractImageTagFromRequest(headers, body);
expect(tag).toBeNull();
});
it("should return null when package_url has no tag", () => {
const headers = { "x-github-event": "registry_package" };
const body = {
registry_package: {
package_version: {
version: "sha256:abc123...",
package_url: "ghcr.io/owner/repo",
},
},
};
const tag = extractImageTagFromRequest(headers, body);
expect(tag).toBeNull();
});
it("should return null when package_url ends with colon (no tag)", () => {
const headers = { "x-github-event": "registry_package" };
const body = {
registry_package: {
package_version: {
version: "sha256:abc123...",
package_url: "ghcr.io/owner/repo:",
container_metadata: {
tag: {
name: "",
digest: "sha256:abc123...",
},
},
},
},
};
const tag = extractImageTagFromRequest(headers, body);
expect(tag).toBeNull();
});
it("should return null when tag name is empty string", () => {
const headers = { "x-github-event": "registry_package" };
const body = {
registry_package: {
package_version: {
version: "sha256:abc123...",
container_metadata: {
tag: {
name: "",
digest: "sha256:abc123...",
},
},
package_url: "ghcr.io/owner/repo:",
},
},
};
const tag = extractImageTagFromRequest(headers, body);
expect(tag).toBeNull();
});
it("should ignore tag if it matches the version (digest)", () => {
const headers = { "x-github-event": "registry_package" };
const body = {
registry_package: {
package_version: {
version: "sha256:abc123...",
container_metadata: {
tag: {
name: "sha256:abc123...",
digest: "sha256:abc123...",
},
},
package_url: "ghcr.io/owner/repo:latest",
},
},
};
const tag = extractImageTagFromRequest(headers, body);
expect(tag).toBe("latest");
});
it("should handle registry_package commit message with package_url", () => {
const headers = { "x-github-event": "registry_package" };
const body = {
registry_package: {
package_version: {
package_url: "ghcr.io/owner/repo:latest",
},
},
};
const message = extractCommitMessage(headers, body);
expect(message).toBe("Docker GHCR image pushed: ghcr.io/owner/repo:latest");
});
it("should handle registry_package commit message when package_url is missing", () => {
const headers = { "x-github-event": "registry_package" };
const body = {
registry_package: {
package_version: {
version: "sha256:abc123...",
},
},
};
const message = extractCommitMessage(headers, body);
expect(message).toBe("Docker GHCR image pushed");
});
it("should handle registry_package commit message when package_version is missing", () => {
const headers = { "x-github-event": "registry_package" };
const body = {
registry_package: {},
};
const message = extractCommitMessage(headers, body);
expect(message).toBe("NEW COMMIT");
});
});
describe("Docker Image Name and Tag Extraction", () => {
describe("extractImageName", () => {
it("should return image name without tag", () => {
expect(extractImageName("my-image:latest")).toBe("my-image");
expect(extractImageName("my-image:1.0.0")).toBe("my-image");
expect(extractImageName("ghcr.io/owner/repo:latest")).toBe(
"ghcr.io/owner/repo",
);
});
it("should return full image name when no tag is present", () => {
expect(extractImageName("my-image")).toBe("my-image");
expect(extractImageName("ghcr.io/owner/repo")).toBe("ghcr.io/owner/repo");
});
it("should handle images with port numbers correctly", () => {
expect(extractImageName("registry:5000/image:tag")).toBe(
"registry:5000/image",
);
expect(extractImageName("localhost:5000/my-app:latest")).toBe(
"localhost:5000/my-app",
);
});
it("should handle complex image paths", () => {
expect(
extractImageName("myregistryhost:5000/fedora/httpd:version1.0"),
).toBe("myregistryhost:5000/fedora/httpd");
expect(extractImageName("registry.example.com:8080/ns/app:v1.2.3")).toBe(
"registry.example.com:8080/ns/app",
);
});
it("should return null for invalid inputs", () => {
expect(extractImageName(null)).toBeNull();
expect(extractImageName("")).toBeNull();
});
it("should handle edge cases with multiple colons", () => {
expect(extractImageName("image:tag:extra")).toBe("image:tag");
expect(extractImageName("registry:5000:invalid")).toBe("registry:5000");
});
});
describe("extractImageTag", () => {
it("should extract tag from image with tag", () => {
expect(extractImageTag("my-image:latest")).toBe("latest");
expect(extractImageTag("my-image:1.0.0")).toBe("1.0.0");
expect(extractImageTag("ghcr.io/owner/repo:v1.2.3")).toBe("v1.2.3");
});
it("should return 'latest' when no tag is present", () => {
expect(extractImageTag("my-image")).toBe("latest");
expect(extractImageTag("ghcr.io/owner/repo")).toBe("latest");
});
it("should handle complex image paths with tags", () => {
expect(
extractImageTag("myregistryhost:5000/fedora/httpd:version1.0"),
).toBe("version1.0");
expect(extractImageTag("registry.example.com:8080/ns/app:v1.2.3")).toBe(
"v1.2.3",
);
});
it("should return null for invalid inputs", () => {
expect(extractImageTag(null)).toBeNull();
expect(extractImageTag("")).toBeNull();
});
it("should handle edge cases with multiple colons", () => {
expect(extractImageTag("image:tag:extra")).toBe("extra");
expect(extractImageTag("registry:5000/image:tag")).toBe("tag");
});
it("should handle numeric tags", () => {
expect(extractImageTag("my-image:123")).toBe("123");
expect(extractImageTag("my-image:1")).toBe("1");
});
});
});
+1
View File
@@ -42,6 +42,7 @@ const baseApp: ApplicationNested = {
triggerType: "push", triggerType: "push",
appName: "", appName: "",
autoDeploy: true, autoDeploy: true,
endpointSpecSwarm: null,
serverId: "", serverId: "",
registryUrl: "", registryUrl: "",
branch: null, branch: null,
@@ -15,6 +15,7 @@ const baseApp: ApplicationNested = {
giteaId: "", giteaId: "",
cleanCache: false, cleanCache: false,
applicationStatus: "done", applicationStatus: "done",
endpointSpecSwarm: null,
appName: "", appName: "",
autoDeploy: true, autoDeploy: true,
enableSubmodules: false, enableSubmodules: false,
@@ -122,6 +122,22 @@ const NetworkSwarmSchema = z.array(
const LabelsSwarmSchema = z.record(z.string()); const LabelsSwarmSchema = z.record(z.string());
const EndpointPortConfigSwarmSchema = z
.object({
Protocol: z.string().optional(),
TargetPort: z.number().optional(),
PublishedPort: z.number().optional(),
PublishMode: z.string().optional(),
})
.strict();
const EndpointSpecSwarmSchema = z
.object({
Mode: z.string().optional(),
Ports: z.array(EndpointPortConfigSwarmSchema).optional(),
})
.strict();
const createStringToJSONSchema = (schema: z.ZodTypeAny) => { const createStringToJSONSchema = (schema: z.ZodTypeAny) => {
return z return z
.string() .string()
@@ -178,6 +194,9 @@ const addSwarmSettings = z.object({
labelsSwarm: createStringToJSONSchema(LabelsSwarmSchema).nullable(), labelsSwarm: createStringToJSONSchema(LabelsSwarmSchema).nullable(),
networkSwarm: createStringToJSONSchema(NetworkSwarmSchema).nullable(), networkSwarm: createStringToJSONSchema(NetworkSwarmSchema).nullable(),
stopGracePeriodSwarm: z.bigint().nullable(), stopGracePeriodSwarm: z.bigint().nullable(),
endpointSpecSwarm: createStringToJSONSchema(
EndpointSpecSwarmSchema,
).nullable(),
}); });
type AddSwarmSettings = z.infer<typeof addSwarmSettings>; type AddSwarmSettings = z.infer<typeof addSwarmSettings>;
@@ -234,6 +253,7 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
labelsSwarm: null, labelsSwarm: null,
networkSwarm: null, networkSwarm: null,
stopGracePeriodSwarm: null, stopGracePeriodSwarm: null,
endpointSpecSwarm: null,
}, },
resolver: zodResolver(addSwarmSettings), resolver: zodResolver(addSwarmSettings),
}); });
@@ -275,6 +295,9 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
? JSON.stringify(data.networkSwarm, null, 2) ? JSON.stringify(data.networkSwarm, null, 2)
: null, : null,
stopGracePeriodSwarm: normalizedStopGracePeriod, stopGracePeriodSwarm: normalizedStopGracePeriod,
endpointSpecSwarm: data.endpointSpecSwarm
? JSON.stringify(data.endpointSpecSwarm, null, 2)
: null,
}); });
} }
}, [form, form.reset, data]); }, [form, form.reset, data]);
@@ -296,6 +319,7 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
labelsSwarm: data.labelsSwarm, labelsSwarm: data.labelsSwarm,
networkSwarm: data.networkSwarm, networkSwarm: data.networkSwarm,
stopGracePeriodSwarm: data.stopGracePeriodSwarm ?? null, stopGracePeriodSwarm: data.stopGracePeriodSwarm ?? null,
endpointSpecSwarm: data.endpointSpecSwarm,
}) })
.then(async () => { .then(async () => {
toast.success("Swarm settings updated"); toast.success("Swarm settings updated");
@@ -846,6 +870,67 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="endpointSpecSwarm"
render={({ field }) => (
<FormItem className="relative ">
<FormLabel>Endpoint Spec</FormLabel>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<FormDescription className="break-all w-fit flex flex-row gap-1 items-center">
Check the interface
<HelpCircle className="size-4 text-muted-foreground" />
</FormDescription>
</TooltipTrigger>
<TooltipContent
className="w-full z-[999]"
align="start"
side="bottom"
>
<code>
<pre>
{`{
Mode?: string | undefined;
Ports?: Array<{
Protocol?: string | undefined;
TargetPort?: number | undefined;
PublishedPort?: number | undefined;
PublishMode?: string | undefined;
}> | undefined;
}`}
</pre>
</code>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<FormControl>
<CodeEditor
language="json"
placeholder={`{
"Mode": "dnsrr",
"Ports": [
{
"Protocol": "tcp",
"TargetPort": 5432,
"PublishedPort": 5432,
"PublishMode": "host"
}
]
}`}
className="h-[17rem] font-mono"
{...field}
value={field?.value || ""}
/>
</FormControl>
<pre>
<FormMessage />
</pre>
</FormItem>
)}
/>
<DialogFooter className="flex w-full flex-row justify-end md:col-span-2 m-0 sticky bottom-0 right-0 bg-muted border"> <DialogFooter className="flex w-full flex-row justify-end md:col-span-2 m-0 sticky bottom-0 right-0 bg-muted border">
<Button <Button
isLoading={isLoading} isLoading={isLoading}
@@ -59,7 +59,13 @@ const mySchema = z.discriminatedUnion("type", [
z z
.object({ .object({
type: z.literal("volume"), type: z.literal("volume"),
volumeName: z.string().min(1, "Volume name required"), volumeName: z
.string()
.min(1, "Volume name required")
.regex(
/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/,
"Invalid volume name. Use letters, numbers, '._-' and start with a letter/number.",
),
}) })
.merge(mountSchema), .merge(mountSchema),
z z
@@ -41,7 +41,13 @@ const mySchema = z.discriminatedUnion("type", [
z z
.object({ .object({
type: z.literal("volume"), type: z.literal("volume"),
volumeName: z.string().min(1, "Volume name required"), volumeName: z
.string()
.min(1, "Volume name required")
.regex(
/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/,
"Invalid volume name. Use letters, numbers, '._-' and start with a letter/number.",
),
}) })
.merge(mountSchema), .merge(mountSchema),
z z
@@ -1,4 +1,12 @@
import { Clock, Loader2, RefreshCcw, RocketIcon, Settings } from "lucide-react"; import {
ChevronDown,
ChevronUp,
Clock,
Loader2,
RefreshCcw,
RocketIcon,
Settings,
} from "lucide-react";
import React, { useEffect, useMemo, useState } from "react"; import React, { useEffect, useMemo, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
@@ -80,6 +88,23 @@ export const ShowDeployments = ({
} = api.compose.cancelDeployment.useMutation(); } = api.compose.cancelDeployment.useMutation();
const [url, setUrl] = React.useState(""); const [url, setUrl] = React.useState("");
const [expandedDescriptions, setExpandedDescriptions] = useState<Set<string>>(
new Set(),
);
const MAX_DESCRIPTION_LENGTH = 200;
const truncateDescription = (description: string): string => {
if (description.length <= MAX_DESCRIPTION_LENGTH) {
return description;
}
const truncated = description.slice(0, MAX_DESCRIPTION_LENGTH);
const lastSpace = truncated.lastIndexOf(" ");
if (lastSpace > MAX_DESCRIPTION_LENGTH - 20 && lastSpace > 0) {
return `${truncated.slice(0, lastSpace)}...`;
}
return `${truncated}...`;
};
// Check for stuck deployment (more than 9 minutes) - only for the most recent deployment // Check for stuck deployment (more than 9 minutes) - only for the most recent deployment
const stuckDeployment = useMemo(() => { const stuckDeployment = useMemo(() => {
@@ -217,118 +242,164 @@ export const ShowDeployments = ({
</div> </div>
) : ( ) : (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{deployments?.map((deployment, index) => ( {deployments?.map((deployment, index) => {
<div const titleText = deployment?.title?.trim() || "";
key={deployment.deploymentId} const needsTruncation = titleText.length > MAX_DESCRIPTION_LENGTH;
className="flex items-center justify-between rounded-lg border p-4 gap-2" const isExpanded = expandedDescriptions.has(
> deployment.deploymentId,
<div className="flex flex-col"> );
<span className="flex items-center gap-4 font-medium capitalize text-foreground">
{index + 1}. {deployment.status} return (
<StatusTooltip <div
status={deployment?.status} key={deployment.deploymentId}
className="size-2.5" className="flex items-center justify-between rounded-lg border p-4 gap-2"
/> >
</span> <div className="flex flex-col">
<span className="text-sm text-muted-foreground"> <span className="flex items-center gap-4 font-medium capitalize text-foreground">
{deployment.title} {index + 1}. {deployment.status}
</span> <StatusTooltip
{deployment.description && ( status={deployment?.status}
<span className="break-all text-sm text-muted-foreground"> className="size-2.5"
{deployment.description} />
</span> </span>
)}
</div>
<div className="flex flex-col items-end gap-2">
<div className="text-sm capitalize text-muted-foreground flex items-center gap-2">
<DateTooltip date={deployment.createdAt} />
{deployment.startedAt && deployment.finishedAt && (
<Badge
variant="outline"
className="text-[10px] gap-1 flex items-center"
>
<Clock className="size-3" />
{formatDuration(
Math.floor(
(new Date(deployment.finishedAt).getTime() -
new Date(deployment.startedAt).getTime()) /
1000,
),
)}
</Badge>
)}
</div>
<div className="flex flex-row items-center gap-2"> <div className="flex flex-col gap-1">
{deployment.pid && deployment.status === "running" && ( <span className="break-words text-sm text-muted-foreground whitespace-pre-wrap">
<DialogAction {isExpanded || !needsTruncation
title="Kill Process" ? titleText
description="Are you sure you want to kill the process?" : truncateDescription(titleText)}
type="default" </span>
onClick={async () => { {needsTruncation && (
await killProcess({ <button
deploymentId: deployment.deploymentId, type="button"
}) onClick={() => {
.then(() => { const next = new Set(expandedDescriptions);
toast.success("Process killed successfully"); if (next.has(deployment.deploymentId)) {
}) next.delete(deployment.deploymentId);
.catch(() => { } else {
toast.error("Error killing process"); next.add(deployment.deploymentId);
}); }
}} setExpandedDescriptions(next);
> }}
<Button className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors w-fit mt-1 cursor-pointer"
variant="destructive" aria-label={
size="sm" isExpanded
isLoading={isKillingProcess} ? "Collapse commit message"
: "Expand commit message"
}
> >
Kill Process {isExpanded ? (
</Button> <>
</DialogAction> <ChevronUp className="size-3" />
)} Show less
<Button </>
onClick={() => { ) : (
setActiveLog(deployment); <>
}} <ChevronDown className="size-3" />
> Show more
View </>
</Button> )}
</button>
)}
{/* Hash (from description) - shown in compact form */}
{deployment.description?.trim() && (
<span className="text-xs text-muted-foreground font-mono">
{deployment.description}
</span>
)}
</div>
</div>
<div className="flex flex-col items-end gap-2 max-w-[300px] w-full justify-start">
<div className="text-sm capitalize text-muted-foreground flex items-center gap-2">
<DateTooltip date={deployment.createdAt} />
{deployment.startedAt && deployment.finishedAt && (
<Badge
variant="outline"
className="text-[10px] gap-1 flex items-center"
>
<Clock className="size-3" />
{formatDuration(
Math.floor(
(new Date(deployment.finishedAt).getTime() -
new Date(deployment.startedAt).getTime()) /
1000,
),
)}
</Badge>
)}
</div>
{deployment?.rollback && <div className="flex flex-row items-center gap-2">
deployment.status === "done" && {deployment.pid && deployment.status === "running" && (
type === "application" && (
<DialogAction <DialogAction
title="Rollback to this deployment" title="Kill Process"
description="Are you sure you want to rollback to this deployment?" description="Are you sure you want to kill the process?"
type="default" type="default"
onClick={async () => { onClick={async () => {
await rollback({ await killProcess({
rollbackId: deployment.rollback.rollbackId, deploymentId: deployment.deploymentId,
}) })
.then(() => { .then(() => {
toast.success( toast.success("Process killed successfully");
"Rollback initiated successfully",
);
}) })
.catch(() => { .catch(() => {
toast.error("Error initiating rollback"); toast.error("Error killing process");
}); });
}} }}
> >
<Button <Button
variant="secondary" variant="destructive"
size="sm" size="sm"
isLoading={isRollingBack} isLoading={isKillingProcess}
> >
<RefreshCcw className="size-4 text-primary group-hover:text-red-500" /> Kill Process
Rollback
</Button> </Button>
</DialogAction> </DialogAction>
)} )}
<Button
onClick={() => {
setActiveLog(deployment);
}}
>
View
</Button>
{deployment?.rollback &&
deployment.status === "done" &&
type === "application" && (
<DialogAction
title="Rollback to this deployment"
description="Are you sure you want to rollback to this deployment?"
type="default"
onClick={async () => {
await rollback({
rollbackId: deployment.rollback.rollbackId,
})
.then(() => {
toast.success(
"Rollback initiated successfully",
);
})
.catch(() => {
toast.error("Error initiating rollback");
});
}}
>
<Button
variant="secondary"
size="sm"
isLoading={isRollingBack}
>
<RefreshCcw className="size-4 text-primary group-hover:text-red-500" />
Rollback
</Button>
</DialogAction>
)}
</div>
</div> </div>
</div> </div>
</div> );
))} })}
</div> </div>
)} )}
<ShowDeployment <ShowDeployment
@@ -47,7 +47,13 @@ const formSchema = z
.object({ .object({
name: z.string().min(1, "Name is required"), name: z.string().min(1, "Name is required"),
cronExpression: z.string().min(1, "Cron expression is required"), cronExpression: z.string().min(1, "Cron expression is required"),
volumeName: z.string().min(1, "Volume name is required"), volumeName: z
.string()
.min(1, "Volume name is required")
.regex(
/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/,
"Invalid volume name. Use letters, numbers, '._-' and start with a letter/number.",
),
prefix: z.string(), prefix: z.string(),
keepLatestCount: z.coerce keepLatestCount: z.coerce
.number() .number()
@@ -86,7 +86,7 @@ export const ShowVolumeBackups = ({
</CardTitle> </CardTitle>
<CardDescription> <CardDescription>
Schedule volume backups to run automatically at specified Schedule volume backups to run automatically at specified
intervals. intervals
</CardDescription> </CardDescription>
</div> </div>
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
@@ -1,6 +1,6 @@
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import copy from "copy-to-clipboard"; import copy from "copy-to-clipboard";
import { debounce } from "lodash"; import _ from "lodash";
import { import {
CheckIcon, CheckIcon,
ChevronsUpDown, ChevronsUpDown,
@@ -236,7 +236,7 @@ export const RestoreBackup = ({
const currentDatabaseType = form.watch("databaseType"); const currentDatabaseType = form.watch("databaseType");
const metadata = form.watch("metadata"); const metadata = form.watch("metadata");
const debouncedSetSearch = debounce((value: string) => { const debouncedSetSearch = _.debounce((value: string) => {
setDebouncedSearchTerm(value); setDebouncedSearchTerm(value);
}, 350); }, 350);
@@ -1,5 +1,5 @@
import { FancyAnsi } from "fancy-ansi"; import { FancyAnsi } from "fancy-ansi";
import { escapeRegExp } from "lodash"; import _ from "lodash";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { import {
Tooltip, Tooltip,
@@ -47,7 +47,7 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) {
} }
const htmlContent = fancyAnsi.toHtml(text); const htmlContent = fancyAnsi.toHtml(text);
const searchRegex = new RegExp(`(${escapeRegExp(term)})`, "gi"); const searchRegex = new RegExp(`(${_.escapeRegExp(term)})`, "gi");
const modifiedContent = htmlContent.replace( const modifiedContent = htmlContent.replace(
searchRegex, searchRegex,
@@ -14,6 +14,7 @@ import { useEffect, useMemo, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar"; import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { DateTooltip } from "@/components/shared/date-tooltip"; import { DateTooltip } from "@/components/shared/date-tooltip";
import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input";
import { StatusTooltip } from "@/components/shared/status-tooltip"; import { StatusTooltip } from "@/components/shared/status-tooltip";
import { import {
AlertDialog, AlertDialog,
@@ -44,7 +45,6 @@ import {
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input";
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -52,12 +52,14 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { TimeBadge } from "@/components/ui/time-badge";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { HandleProject } from "./handle-project"; import { HandleProject } from "./handle-project";
import { ProjectEnvironment } from "./project-environment"; import { ProjectEnvironment } from "./project-environment";
export const ShowProjects = () => { export const ShowProjects = () => {
const utils = api.useUtils(); const utils = api.useUtils();
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data, isLoading } = api.project.all.useQuery(); const { data, isLoading } = api.project.all.useQuery();
const { data: auth } = api.user.get.useQuery(); const { data: auth } = api.user.get.useQuery();
const { mutateAsync } = api.project.remove.useMutation(); const { mutateAsync } = api.project.remove.useMutation();
@@ -135,6 +137,11 @@ export const ShowProjects = () => {
<BreadcrumbSidebar <BreadcrumbSidebar
list={[{ name: "Projects", href: "/dashboard/projects" }]} list={[{ name: "Projects", href: "/dashboard/projects" }]}
/> />
{!isCloud && (
<div className="absolute top-5 right-5">
<TimeBadge />
</div>
)}
<div className="w-full"> <div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl "> <Card className="h-full bg-sidebar p-2.5 rounded-xl ">
<div className="rounded-xl bg-background shadow-md "> <div className="rounded-xl bg-background shadow-md ">
@@ -148,7 +155,6 @@ export const ShowProjects = () => {
Create and manage your projects Create and manage your projects
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
{(auth?.role === "owner" || auth?.canCreateProjects) && ( {(auth?.role === "owner" || auth?.canCreateProjects) && (
<div className=""> <div className="">
<HandleProject /> <HandleProject />
@@ -298,7 +304,13 @@ export const ShowProjects = () => {
<Link <Link
className="space-x-4 text-xs cursor-pointer justify-between" className="space-x-4 text-xs cursor-pointer justify-between"
target="_blank" target="_blank"
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`} href={`${
domain.https
? "https"
: "http"
}://${domain.host}${
domain.path
}`}
> >
<span className="truncate"> <span className="truncate">
{domain.host} {domain.host}
@@ -340,7 +352,13 @@ export const ShowProjects = () => {
<Link <Link
className="space-x-4 text-xs cursor-pointer justify-between" className="space-x-4 text-xs cursor-pointer justify-between"
target="_blank" target="_blank"
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`} href={`${
domain.https
? "https"
: "http"
}://${domain.host}${
domain.path
}`}
> >
<span className="truncate"> <span className="truncate">
{domain.host} {domain.host}
@@ -1,4 +1,3 @@
import type { findEnvironmentById } from "@dokploy/server/index";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
@@ -27,12 +26,10 @@ import {
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api"; import { api, type RouterOutputs } from "@/utils/api";
type Environment = Omit< type Project = RouterOutputs["project"]["all"][number];
Awaited<ReturnType<typeof findEnvironmentById>>, type Environment = Project["environments"][number];
"project"
>;
export type Services = { export type Services = {
appName: string; appName: string;
@@ -53,17 +50,16 @@ export type Services = {
}; };
export const extractServices = (data: Environment | undefined) => { export const extractServices = (data: Environment | undefined) => {
const applications: Services[] = const applications: Services[] = (data?.applications?.map((item) => ({
data?.applications.map((item) => ({ appName: item.appName,
appName: item.appName, name: item.name,
name: item.name, type: "application",
type: "application", id: item.applicationId,
id: item.applicationId, createdAt: item.createdAt,
createdAt: item.createdAt, status: item.applicationStatus,
status: item.applicationStatus, description: item.description,
description: item.description, serverId: item.serverId,
serverId: item.serverId, })) ?? []) as Services[];
})) || [];
const mariadb: Services[] = const mariadb: Services[] =
data?.mariadb.map((item) => ({ data?.mariadb.map((item) => ({
@@ -125,17 +121,16 @@ export const extractServices = (data: Environment | undefined) => {
serverId: item.serverId, serverId: item.serverId,
})) || []; })) || [];
const compose: Services[] = const compose: Services[] = (data?.compose?.map((item) => ({
data?.compose.map((item) => ({ appName: item.appName,
appName: item.appName, name: item.name,
name: item.name, type: "compose",
type: "compose", id: item.composeId,
id: item.composeId, createdAt: item.createdAt,
createdAt: item.createdAt, status: item.composeStatus,
status: item.composeStatus, description: item.description,
description: item.description, serverId: item.serverId,
serverId: item.serverId, })) ?? []) as Services[];
})) || [];
applications.push( applications.push(
...mysql, ...mysql,
+111 -46
View File
@@ -26,6 +26,7 @@ import {
PieChart, PieChart,
Server, Server,
ShieldCheck, ShieldCheck,
Star,
Trash2, Trash2,
User, User,
Users, Users,
@@ -82,6 +83,7 @@ import { AddOrganization } from "../dashboard/organization/handle-organization";
import { DialogAction } from "../shared/dialog-action"; import { DialogAction } from "../shared/dialog-action";
import { Logo } from "../shared/logo"; import { Logo } from "../shared/logo";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { TimeBadge } from "../ui/time-badge";
import { UpdateServerButton } from "./update-server"; import { UpdateServerButton } from "./update-server";
import { UserNav } from "./user-nav"; import { UserNav } from "./user-nav";
@@ -497,7 +499,6 @@ function SidebarLogo() {
const { data: isCloud } = api.settings.isCloud.useQuery(); const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: user } = api.user.get.useQuery(); const { data: user } = api.user.get.useQuery();
const { data: session } = authClient.useSession(); const { data: session } = authClient.useSession();
const { const {
data: organizations, data: organizations,
refetch, refetch,
@@ -505,6 +506,8 @@ function SidebarLogo() {
} = api.organization.all.useQuery(); } = api.organization.all.useQuery();
const { mutateAsync: deleteOrganization, isLoading: isRemoving } = const { mutateAsync: deleteOrganization, isLoading: isRemoving } =
api.organization.delete.useMutation(); api.organization.delete.useMutation();
const { mutateAsync: setDefaultOrganization, isLoading: isSettingDefault } =
api.organization.setDefault.useMutation();
const { isMobile } = useSidebar(); const { isMobile } = useSidebar();
const { data: activeOrganization } = authClient.useActiveOrganization(); const { data: activeOrganization } = authClient.useActiveOrganization();
const _utils = api.useUtils(); const _utils = api.useUtils();
@@ -594,66 +597,127 @@ function SidebarLogo() {
<DropdownMenuLabel className="text-xs text-muted-foreground"> <DropdownMenuLabel className="text-xs text-muted-foreground">
Organizations Organizations
</DropdownMenuLabel> </DropdownMenuLabel>
{organizations?.map((org) => ( {organizations?.map((org) => {
<div className="flex flex-row justify-between" key={org.name}> const isDefault = org.members?.[0]?.isDefault ?? false;
<DropdownMenuItem return (
onClick={async () => { <div
await authClient.organization.setActive({ className="flex flex-row justify-between"
organizationId: org.id, key={org.name}
});
window.location.reload();
}}
className="w-full gap-2 p-2"
> >
<div className="flex flex-col gap-4">{org.name}</div> <DropdownMenuItem
<div className="flex size-6 items-center justify-center rounded-sm border"> onClick={async () => {
<Logo await authClient.organization.setActive({
className={cn( organizationId: org.id,
"transition-all", });
state === "collapsed" ? "size-6" : "size-10", window.location.reload();
)} }}
logoUrl={org.logo ?? undefined} className="w-full gap-2 p-2"
/> >
</div> <div className="flex flex-col gap-1">
</DropdownMenuItem> <div className="flex items-center gap-2">
{org.ownerId === session?.user?.id && ( {org.name}
</div>
</div>
<div className="flex size-6 items-center justify-center rounded-sm border">
<Logo
className={cn(
"transition-all",
state === "collapsed" ? "size-6" : "size-10",
)}
logoUrl={org.logo ?? undefined}
/>
</div>
</DropdownMenuItem>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<AddOrganization organizationId={org.id} /> <Button
<DialogAction variant="ghost"
title="Delete Organization" size="icon"
description="Are you sure you want to delete this organization?" className={cn(
type="destructive" "group",
onClick={async () => { isDefault
await deleteOrganization({ ? "hover:bg-yellow-500/10"
: "hover:bg-blue-500/10",
)}
isLoading={isSettingDefault && !isDefault}
disabled={isDefault}
onClick={async (e) => {
if (isDefault) return;
e.stopPropagation();
await setDefaultOrganization({
organizationId: org.id, organizationId: org.id,
}) })
.then(() => { .then(() => {
refetch(); refetch();
toast.success( toast.success("Default organization updated");
"Organization deleted successfully",
);
}) })
.catch((error) => { .catch((error) => {
toast.error( toast.error(
error?.message || error?.message ||
"Error deleting organization", "Error setting default organization",
); );
}); });
}} }}
title={
isDefault
? "Default organization"
: "Set as default"
}
> >
<Button {isDefault ? (
variant="ghost" <Star
size="icon" fill="#eab308"
className="group hover:bg-red-500/10" stroke="#eab308"
isLoading={isRemoving} className="size-4 text-yellow-500"
> />
<Trash2 className="size-4 text-primary group-hover:text-red-500" /> ) : (
</Button> <Star
</DialogAction> fill="none"
stroke="currentColor"
className="size-4 text-gray-400 group-hover:text-blue-500 transition-colors"
/>
)}
</Button>
{org.ownerId === session?.user?.id && (
<>
<AddOrganization organizationId={org.id} />
<DialogAction
title="Delete Organization"
description="Are you sure you want to delete this organization?"
type="destructive"
onClick={async () => {
await deleteOrganization({
organizationId: org.id,
})
.then(() => {
refetch();
toast.success(
"Organization deleted successfully",
);
})
.catch((error) => {
toast.error(
error?.message ||
"Error deleting organization",
);
});
}}
>
<Button
variant="ghost"
size="icon"
className="group hover:bg-red-500/10"
isLoading={isRemoving}
>
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
</Button>
</DialogAction>
</>
)}
</div> </div>
)} </div>
</div> );
))} })}
{(user?.role === "owner" || isCloud) && ( {(user?.role === "owner" || isCloud) && (
<> <>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
@@ -1062,6 +1126,7 @@ export default function Page({ children }: Props) {
</BreadcrumbList> </BreadcrumbList>
</Breadcrumb> </Breadcrumb>
</div> </div>
{!isCloud && <TimeBadge />}
</div> </div>
</header> </header>
)} )}
+4 -3
View File
@@ -67,9 +67,10 @@ export const Dropzone = React.forwardRef<HTMLDivElement, DropzoneProps>(
ref={inputRef} ref={inputRef}
type="file" type="file"
className={cn("hidden", className)} className={cn("hidden", className)}
onChange={(e: ChangeEvent<HTMLInputElement>) => onChange={(e: ChangeEvent<HTMLInputElement>) => {
onChange(e.target.files) onChange(e.target.files);
} e.target.value = "";
}}
/> />
</div> </div>
</CardContent> </CardContent>
+58
View File
@@ -0,0 +1,58 @@
"use client";
import { useEffect, useState } from "react";
import { api } from "@/utils/api";
export function TimeBadge() {
const { data: serverTime } = api.server.getServerTime.useQuery(undefined);
const [time, setTime] = useState<Date | null>(null);
useEffect(() => {
if (serverTime?.time) {
setTime(new Date(serverTime.time));
}
}, [serverTime]);
useEffect(() => {
const timer = setInterval(() => {
setTime((prevTime) => {
if (!prevTime) return null;
const newTime = new Date(prevTime.getTime() + 1000);
return newTime;
});
}, 1000);
return () => {
clearInterval(timer);
};
}, []);
if (!time || !serverTime?.timezone) {
return null;
}
const getUtcOffset = (timeZone: string) => {
const date = new Date();
const utcDate = new Date(date.toLocaleString("en-US", { timeZone: "UTC" }));
const tzDate = new Date(date.toLocaleString("en-US", { timeZone }));
const offset = (tzDate.getTime() - utcDate.getTime()) / (1000 * 60 * 60);
const sign = offset >= 0 ? "+" : "-";
const hours = Math.floor(Math.abs(offset));
const minutes = (Math.abs(offset) * 60) % 60;
return `UTC${sign}${hours.toString().padStart(2, "0")}:${minutes
.toString()
.padStart(2, "0")}`;
};
return (
<div className="inline-flex items-center gap-2 rounded-md border px-2 py-1 text-xs sm:text-sm whitespace-nowrap max-w-full overflow-hidden">
<span className="hidden sm:inline">Server Time:</span>
<span className="font-medium tabular-nums">
{time.toLocaleTimeString()}
</span>
<span className="hidden sm:inline text-muted-foreground">
({serverTime.timezone} | {getUtcOffset(serverTime.timezone)})
</span>
</div>
);
}
@@ -0,0 +1 @@
ALTER TABLE "member" ADD COLUMN "is_default" boolean DEFAULT false NOT NULL;
@@ -0,0 +1,39 @@
ALTER TABLE "user_temp" RENAME TO "user";--> statement-breakpoint
ALTER TABLE "user" DROP CONSTRAINT "user_temp_email_unique";--> statement-breakpoint
ALTER TABLE "account" DROP CONSTRAINT "account_user_id_user_temp_id_fk";
--> statement-breakpoint
ALTER TABLE "apikey" DROP CONSTRAINT "apikey_user_id_user_temp_id_fk";
--> statement-breakpoint
ALTER TABLE "invitation" DROP CONSTRAINT "invitation_inviter_id_user_temp_id_fk";
--> statement-breakpoint
ALTER TABLE "member" DROP CONSTRAINT "member_user_id_user_temp_id_fk";
--> statement-breakpoint
ALTER TABLE "organization" DROP CONSTRAINT "organization_owner_id_user_temp_id_fk";
--> statement-breakpoint
ALTER TABLE "two_factor" DROP CONSTRAINT "two_factor_user_id_user_temp_id_fk";
--> statement-breakpoint
ALTER TABLE "backup" DROP CONSTRAINT "backup_userId_user_temp_id_fk";
--> statement-breakpoint
ALTER TABLE "git_provider" DROP CONSTRAINT "git_provider_userId_user_temp_id_fk";
--> statement-breakpoint
ALTER TABLE "schedule" DROP CONSTRAINT "schedule_userId_user_temp_id_fk";
--> statement-breakpoint
ALTER TABLE "session_temp" DROP CONSTRAINT "session_temp_user_id_user_temp_id_fk";
--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "endpointSpecSwarm" json;--> statement-breakpoint
ALTER TABLE "mariadb" ADD COLUMN "endpointSpecSwarm" json;--> statement-breakpoint
ALTER TABLE "mongo" ADD COLUMN "endpointSpecSwarm" json;--> statement-breakpoint
ALTER TABLE "mysql" ADD COLUMN "endpointSpecSwarm" json;--> statement-breakpoint
ALTER TABLE "postgres" ADD COLUMN "endpointSpecSwarm" json;--> statement-breakpoint
ALTER TABLE "redis" ADD COLUMN "endpointSpecSwarm" json;--> statement-breakpoint
ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "apikey" ADD CONSTRAINT "apikey_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "invitation" ADD CONSTRAINT "invitation_inviter_id_user_id_fk" FOREIGN KEY ("inviter_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "member" ADD CONSTRAINT "member_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "organization" ADD CONSTRAINT "organization_owner_id_user_id_fk" FOREIGN KEY ("owner_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "two_factor" ADD CONSTRAINT "two_factor_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "backup" ADD CONSTRAINT "backup_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "git_provider" ADD CONSTRAINT "git_provider_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "schedule" ADD CONSTRAINT "schedule_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "session_temp" ADD CONSTRAINT "session_temp_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "user" ADD CONSTRAINT "user_email_unique" UNIQUE("email");
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+14
View File
@@ -834,6 +834,20 @@
"when": 1761415824484, "when": 1761415824484,
"tag": "0118_loose_anita_blake", "tag": "0118_loose_anita_blake",
"breakpoints": true "breakpoints": true
},
{
"idx": 119,
"version": "7",
"when": 1762142756443,
"tag": "0119_bouncy_morbius",
"breakpoints": true
},
{
"idx": 120,
"version": "7",
"when": 1762632540024,
"tag": "0120_lame_captain_midlands",
"breakpoints": true
} }
] ]
} }
+2 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "dokploy", "name": "dokploy",
"version": "v0.25.6", "version": "v0.25.7",
"private": true, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
@@ -120,7 +120,7 @@
"next": "^15.3.2", "next": "^15.3.2",
"next-i18next": "^15.4.2", "next-i18next": "^15.4.2",
"next-themes": "^0.2.1", "next-themes": "^0.2.1",
"node-os-utils": "1.3.7", "node-os-utils": "2.0.1",
"node-pty": "1.0.0", "node-pty": "1.0.0",
"node-schedule": "2.1.1", "node-schedule": "2.1.1",
"nodemailer": "6.9.14", "nodemailer": "6.9.14",
@@ -163,7 +163,6 @@
"@types/lodash": "4.17.4", "@types/lodash": "4.17.4",
"@types/micromatch": "4.0.9", "@types/micromatch": "4.0.9",
"@types/node": "^18.19.104", "@types/node": "^18.19.104",
"@types/node-os-utils": "1.3.4",
"@types/node-schedule": "2.1.6", "@types/node-schedule": "2.1.6",
"@types/nodemailer": "^6.4.17", "@types/nodemailer": "^6.4.17",
"@types/qrcode": "^1.5.5", "@types/qrcode": "^1.5.5",
+170 -9
View File
@@ -12,6 +12,17 @@ import type { DeploymentJob } from "@/server/queues/queue-types";
import { myQueue } from "@/server/queues/queueSetup"; import { myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy"; import { deploy } from "@/server/utils/deploy";
/**
* Helper function to get package_version from registry_package events
*/
const getPackageVersion = (headers: any, body: any) => {
const event = headers["x-github-event"];
if (event === "registry_package") {
return body.registry_package?.package_version;
}
return null;
};
export default async function handler( export default async function handler(
req: NextApiRequest, req: NextApiRequest,
res: NextApiResponse, res: NextApiResponse,
@@ -46,21 +57,60 @@ export default async function handler(
} }
const deploymentTitle = extractCommitMessage(req.headers, req.body); const deploymentTitle = extractCommitMessage(req.headers, req.body);
const deploymentHash = extractHash(req.headers, req.body);
const deploymentHash = extractHash(req.headers, req.body);
const sourceType = application.sourceType; const sourceType = application.sourceType;
if (sourceType === "docker") { if (sourceType === "docker") {
const applicationImageName = extractImageName(application.dockerImage);
const applicationDockerTag = extractImageTag(application.dockerImage); const applicationDockerTag = extractImageTag(application.dockerImage);
const webhookImageName = extractImageNameFromRequest(
req.headers,
req.body,
);
const webhookDockerTag = extractImageTagFromRequest( const webhookDockerTag = extractImageTagFromRequest(
req.headers, req.headers,
req.body, req.body,
); );
if (
applicationDockerTag && if (!applicationImageName) {
webhookDockerTag && res.status(301).json({
webhookDockerTag !== applicationDockerTag message: "Application Docker Image Name Not Found",
) { });
return;
}
if (!webhookImageName) {
res.status(301).json({
message: "Webhook Docker Image Name Not Found",
});
return;
}
// Validate image name matches
if (webhookImageName !== applicationImageName) {
res.status(301).json({
message: `Application Image Name (${applicationImageName}) doesn't match request event payload Image Name (${webhookImageName}).`,
});
return;
}
if (!applicationDockerTag) {
res.status(301).json({
message: "Application Docker Tag Not Found",
});
return;
}
if (!webhookDockerTag) {
res.status(301).json({
message: "Webhook Docker Tag Not Found",
});
return;
}
if (webhookDockerTag !== applicationDockerTag) {
res.status(301).json({ res.status(301).json({
message: `Application Image Tag (${applicationDockerTag}) doesn't match request event payload Image Tag (${webhookDockerTag}).`, message: `Application Image Tag (${applicationDockerTag}) doesn't match request event payload Image Tag (${webhookDockerTag}).`,
}); });
@@ -191,7 +241,7 @@ export default async function handler(
const jobData: DeploymentJob = { const jobData: DeploymentJob = {
applicationId: application.applicationId as string, applicationId: application.applicationId as string,
titleLog: deploymentTitle, titleLog: deploymentTitle,
descriptionLog: `Hash: ${deploymentHash}`, ...(deploymentHash && { descriptionLog: `Hash: ${deploymentHash}` }),
type: "deploy", type: "deploy",
applicationType: "application", applicationType: "application",
server: !!application.serverId, server: !!application.serverId,
@@ -222,6 +272,39 @@ export default async function handler(
} }
} }
/**
* Return the image name without the tag
* Example: "my-image" => "my-image"
* Example: "my-image:latest" => "my-image"
* Example: "my-image:1.0.0" => "my-image"
* Example: "myregistryhost:5000/fedora/httpd:version1.0" => "myregistryhost:5000/fedora/httpd"
* @link https://docs.docker.com/reference/cli/docker/image/tag/
*/
export function extractImageName(dockerImage: string | null): string | null {
if (!dockerImage || typeof dockerImage !== "string") {
return null;
}
// Handle case where there's no tag (no colon or colon is part of port number)
const lastColonIndex = dockerImage.lastIndexOf(":");
if (lastColonIndex === -1) {
return dockerImage;
}
// Check if the part after the last colon looks like a tag (not a port number)
// Port numbers are typically 1-5 digits, tags are usually longer or contain letters
const afterColon = dockerImage.substring(lastColonIndex + 1);
const isPortNumber = /^\d{1,5}$/.test(afterColon);
// If it's a port number (like registry:5000/image), don't split
if (isPortNumber) {
return dockerImage;
}
// Otherwise, split at the last colon to get image name
return dockerImage.substring(0, lastColonIndex);
}
/** /**
* Return the last part of the image name, which is the tag * Return the last part of the image name, which is the tag
* Example: "my-image" => null * Example: "my-image" => null
@@ -230,7 +313,7 @@ export default async function handler(
* Example: "myregistryhost:5000/fedora/httpd:version1.0" => "version1.0" * Example: "myregistryhost:5000/fedora/httpd:version1.0" => "version1.0"
* @link https://docs.docker.com/reference/cli/docker/image/tag/ * @link https://docs.docker.com/reference/cli/docker/image/tag/
*/ */
function extractImageTag(dockerImage: string | null) { export function extractImageTag(dockerImage: string | null) {
if (!dockerImage || typeof dockerImage !== "string") { if (!dockerImage || typeof dockerImage !== "string") {
return null; return null;
} }
@@ -240,12 +323,78 @@ function extractImageTag(dockerImage: string | null) {
} }
/** /**
* Extract the image name (without tag) from webhook request
* @link https://docs.docker.com/docker-hub/webhooks/#example-webhook-payload * @link https://docs.docker.com/docker-hub/webhooks/#example-webhook-payload
* @link https://docs.github.com/en/webhooks/webhook-events-and-payloads#registry_package
*/
export const extractImageNameFromRequest = (
headers: any,
body: any,
): string | null => {
// GitHub Packages: registry_package events (container registry)
const packageVersion = getPackageVersion(headers, body);
if (packageVersion?.package_url) {
const packageUrl = packageVersion.package_url;
// Remove tag if present (everything after the last colon)
if (packageUrl.includes(":")) {
const lastColonIndex = packageUrl.lastIndexOf(":");
// Check if it's a port number (like registry:5000/image)
const afterColon = packageUrl.substring(lastColonIndex + 1);
const isPortNumber = /^\d{1,5}$/.test(afterColon);
if (isPortNumber) {
return packageUrl;
}
return packageUrl.substring(0, lastColonIndex);
}
return packageUrl;
}
// Docker Hub
if (headers["user-agent"]?.includes("Go-http-client")) {
if (body.repository) {
const repoName = body.repository.repo_name;
return `${repoName}`;
}
}
return null;
};
/**
* @link https://docs.docker.com/docker-hub/webhooks/#example-webhook-payload
* @link https://docs.github.com/en/webhooks/webhook-events-and-payloads#registry_package
*/ */
export const extractImageTagFromRequest = ( export const extractImageTagFromRequest = (
headers: any, headers: any,
body: any, body: any,
): string | null => { ): string | null => {
// GitHub Packages: registry_package events (container registry)
const packageVersion = getPackageVersion(headers, body);
if (packageVersion) {
// Try to get tag from container_metadata first (most reliable)
// Only use it if it's not empty and not the same as the version (digest)
const tagName = packageVersion.container_metadata?.tag?.name?.trim() || "";
if (
tagName &&
tagName !== packageVersion.version &&
!tagName.startsWith("sha256:")
) {
return tagName;
}
// Fallback: extract tag from package_url (e.g., "ghcr.io/owner/repo:tag")
if (packageVersion.package_url) {
const packageUrl = packageVersion.package_url;
// Handle case where package_url ends with colon (no tag)
if (packageUrl.endsWith(":")) {
return null;
}
const tagMatch = packageUrl.match(/:([^:]+)$/);
if (tagMatch?.[1]?.trim()) {
return tagMatch[1].trim();
}
}
}
// Docker Hub
if (headers["user-agent"]?.includes("Go-http-client")) { if (headers["user-agent"]?.includes("Go-http-client")) {
if (body.push_data && body.repository) { if (body.push_data && body.repository) {
return body.push_data.tag; return body.push_data.tag;
@@ -255,6 +404,18 @@ export const extractImageTagFromRequest = (
}; };
export const extractCommitMessage = (headers: any, body: any) => { export const extractCommitMessage = (headers: any, body: any) => {
// GitHub Packages: registry_package events (container tags)
const githubEvent = headers["x-github-event"];
if (githubEvent === "registry_package") {
const packageVersion = getPackageVersion(headers, body);
if (packageVersion) {
if (packageVersion.package_url) {
return `Docker GHCR image pushed: ${packageVersion.package_url}`;
}
return "Docker GHCR image pushed";
}
// If package_version is missing, fall through to default behavior
}
// GitHub // GitHub
if (headers["x-github-event"]) { if (headers["x-github-event"]) {
return body.head_commit ? body.head_commit.message : "NEW COMMIT"; return body.head_commit ? body.head_commit.message : "NEW COMMIT";
@@ -283,7 +444,7 @@ export const extractCommitMessage = (headers: any, body: any) => {
if (headers["user-agent"]?.includes("Go-http-client")) { if (headers["user-agent"]?.includes("Go-http-client")) {
if (body.push_data && body.repository) { if (body.push_data && body.repository) {
return `Docker image pushed: ${body.repository.repo_name}:${body.push_data.tag} by ${body.push_data.pusher}`; return `DockerHub image pushed: ${body.repository.repo_name}:${body.push_data.tag} by ${body.push_data.pusher}`;
} }
} }
+20 -30
View File
@@ -4,7 +4,7 @@ import { asc, eq } from "drizzle-orm";
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import Stripe from "stripe"; import Stripe from "stripe";
import { db } from "@/server/db"; import { db } from "@/server/db";
import { organization, server, users_temp } from "@/server/db/schema"; import { organization, server, user } from "@/server/db/schema";
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET!; const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET!;
@@ -64,13 +64,13 @@ export default async function handler(
session.subscription as string, session.subscription as string,
); );
await db await db
.update(users_temp) .update(user)
.set({ .set({
stripeCustomerId: session.customer as string, stripeCustomerId: session.customer as string,
stripeSubscriptionId: session.subscription as string, stripeSubscriptionId: session.subscription as string,
serversQuantity: subscription?.items?.data?.[0]?.quantity ?? 0, serversQuantity: subscription?.items?.data?.[0]?.quantity ?? 0,
}) })
.where(eq(users_temp.id, adminId)) .where(eq(user.id, adminId))
.returning(); .returning();
const admin = await findUserById(adminId); const admin = await findUserById(adminId);
@@ -85,14 +85,12 @@ export default async function handler(
const newSubscription = event.data.object as Stripe.Subscription; const newSubscription = event.data.object as Stripe.Subscription;
await db await db
.update(users_temp) .update(user)
.set({ .set({
stripeSubscriptionId: newSubscription.id, stripeSubscriptionId: newSubscription.id,
stripeCustomerId: newSubscription.customer as string, stripeCustomerId: newSubscription.customer as string,
}) })
.where( .where(eq(user.stripeCustomerId, newSubscription.customer as string))
eq(users_temp.stripeCustomerId, newSubscription.customer as string),
)
.returning(); .returning();
break; break;
@@ -102,14 +100,12 @@ export default async function handler(
const newSubscription = event.data.object as Stripe.Subscription; const newSubscription = event.data.object as Stripe.Subscription;
await db await db
.update(users_temp) .update(user)
.set({ .set({
stripeSubscriptionId: null, stripeSubscriptionId: null,
serversQuantity: 0, serversQuantity: 0,
}) })
.where( .where(eq(user.stripeCustomerId, newSubscription.customer as string));
eq(users_temp.stripeCustomerId, newSubscription.customer as string),
);
const admin = await findUserByStripeCustomerId( const admin = await findUserByStripeCustomerId(
newSubscription.customer as string, newSubscription.customer as string,
@@ -135,24 +131,20 @@ export default async function handler(
if (newSubscription.status === "active") { if (newSubscription.status === "active") {
await db await db
.update(users_temp) .update(user)
.set({ .set({
serversQuantity: newSubscription?.items?.data?.[0]?.quantity ?? 0, serversQuantity: newSubscription?.items?.data?.[0]?.quantity ?? 0,
}) })
.where( .where(eq(user.stripeCustomerId, newSubscription.customer as string));
eq(users_temp.stripeCustomerId, newSubscription.customer as string),
);
const newServersQuantity = admin.serversQuantity; const newServersQuantity = admin.serversQuantity;
await updateServersBasedOnQuantity(admin.id, newServersQuantity); await updateServersBasedOnQuantity(admin.id, newServersQuantity);
} else { } else {
await disableServers(admin.id); await disableServers(admin.id);
await db await db
.update(users_temp) .update(user)
.set({ serversQuantity: 0 }) .set({ serversQuantity: 0 })
.where( .where(eq(user.stripeCustomerId, newSubscription.customer as string));
eq(users_temp.stripeCustomerId, newSubscription.customer as string),
);
} }
break; break;
@@ -172,11 +164,11 @@ export default async function handler(
} }
await db await db
.update(users_temp) .update(user)
.set({ .set({
serversQuantity: suscription?.items?.data?.[0]?.quantity ?? 0, serversQuantity: suscription?.items?.data?.[0]?.quantity ?? 0,
}) })
.where(eq(users_temp.stripeCustomerId, suscription.customer as string)); .where(eq(user.stripeCustomerId, suscription.customer as string));
const admin = await findUserByStripeCustomerId( const admin = await findUserByStripeCustomerId(
suscription.customer as string, suscription.customer as string,
@@ -205,13 +197,11 @@ export default async function handler(
return res.status(400).send("Webhook Error: Admin not found"); return res.status(400).send("Webhook Error: Admin not found");
} }
await db await db
.update(users_temp) .update(user)
.set({ .set({
serversQuantity: 0, serversQuantity: 0,
}) })
.where( .where(eq(user.stripeCustomerId, newInvoice.customer as string));
eq(users_temp.stripeCustomerId, newInvoice.customer as string),
);
await disableServers(admin.id); await disableServers(admin.id);
} }
@@ -229,13 +219,13 @@ export default async function handler(
await disableServers(admin.id); await disableServers(admin.id);
await db await db
.update(users_temp) .update(user)
.set({ .set({
stripeCustomerId: null, stripeCustomerId: null,
stripeSubscriptionId: null, stripeSubscriptionId: null,
serversQuantity: 0, serversQuantity: 0,
}) })
.where(eq(users_temp.stripeCustomerId, customer.id)); .where(eq(user.stripeCustomerId, customer.id));
break; break;
} }
@@ -262,10 +252,10 @@ const disableServers = async (userId: string) => {
}; };
const findUserByStripeCustomerId = async (stripeCustomerId: string) => { const findUserByStripeCustomerId = async (stripeCustomerId: string) => {
const user = db.query.users_temp.findFirst({ const userResult = await db.query.user.findFirst({
where: eq(users_temp.stripeCustomerId, stripeCustomerId), where: eq(user.stripeCustomerId, stripeCustomerId),
}); });
return user; return userResult;
}; };
const activateServer = async (serverId: string) => { const activateServer = async (serverId: string) => {
@@ -1,4 +1,4 @@
import type { findProjectById } from "@dokploy/server"; import type { findEnvironmentById } from "@dokploy/server";
import { validateRequest } from "@dokploy/server/lib/auth"; import { validateRequest } from "@dokploy/server/lib/auth";
import { createServerSideHelpers } from "@trpc/react-query/server"; import { createServerSideHelpers } from "@trpc/react-query/server";
import { import {
@@ -102,6 +102,7 @@ import { api } from "@/utils/api";
export type Services = { export type Services = {
appName: string; appName: string;
serverId?: string | null; serverId?: string | null;
serverName?: string | null;
name: string; name: string;
type: type:
| "mariadb" | "mariadb"
@@ -115,10 +116,10 @@ export type Services = {
id: string; id: string;
createdAt: string; createdAt: string;
status?: "idle" | "running" | "done" | "error"; status?: "idle" | "running" | "done" | "error";
lastDeployDate?: Date | null;
}; };
type Project = Awaited<ReturnType<typeof findProjectById>>; type Environment = Awaited<ReturnType<typeof findEnvironmentById>>;
type Environment = Project["environments"][0];
export const extractServicesFromEnvironment = ( export const extractServicesFromEnvironment = (
environment: Environment | undefined, environment: Environment | undefined,
@@ -128,16 +129,35 @@ export const extractServicesFromEnvironment = (
const allServices: Services[] = []; const allServices: Services[] = [];
const applications: Services[] = const applications: Services[] =
environment.applications?.map((item) => ({ environment.applications?.map((item) => {
appName: item.appName, // Get the most recent deployment date
name: item.name, let lastDeployDate: Date | null = null;
type: "application", const deployments = (item as any).deployments;
id: item.applicationId, if (deployments && deployments.length > 0) {
createdAt: item.createdAt, for (const deployment of deployments) {
status: item.applicationStatus, const deployDate = new Date(
description: item.description, deployment.finishedAt ||
serverId: item.serverId, deployment.startedAt ||
})) || []; deployment.createdAt,
);
if (!lastDeployDate || deployDate > lastDeployDate) {
lastDeployDate = deployDate;
}
}
}
return {
appName: item.appName,
name: item.name,
type: "application",
id: item.applicationId,
createdAt: item.createdAt,
status: item.applicationStatus,
description: item.description,
serverId: item.serverId,
serverName: item?.server?.name || null,
lastDeployDate,
};
}) || [];
const mariadb: Services[] = const mariadb: Services[] =
environment.mariadb?.map((item) => ({ environment.mariadb?.map((item) => ({
@@ -149,6 +169,7 @@ export const extractServicesFromEnvironment = (
status: item.applicationStatus, status: item.applicationStatus,
description: item.description, description: item.description,
serverId: item.serverId, serverId: item.serverId,
serverName: item?.server?.name || null,
})) || []; })) || [];
const postgres: Services[] = const postgres: Services[] =
@@ -161,6 +182,7 @@ export const extractServicesFromEnvironment = (
status: item.applicationStatus, status: item.applicationStatus,
description: item.description, description: item.description,
serverId: item.serverId, serverId: item.serverId,
serverName: item?.server?.name || null,
})) || []; })) || [];
const mongo: Services[] = const mongo: Services[] =
@@ -173,6 +195,7 @@ export const extractServicesFromEnvironment = (
status: item.applicationStatus, status: item.applicationStatus,
description: item.description, description: item.description,
serverId: item.serverId, serverId: item.serverId,
serverName: item?.server?.name || null,
})) || []; })) || [];
const redis: Services[] = const redis: Services[] =
@@ -185,6 +208,7 @@ export const extractServicesFromEnvironment = (
status: item.applicationStatus, status: item.applicationStatus,
description: item.description, description: item.description,
serverId: item.serverId, serverId: item.serverId,
serverName: item?.server?.name || null,
})) || []; })) || [];
const mysql: Services[] = const mysql: Services[] =
@@ -197,19 +221,39 @@ export const extractServicesFromEnvironment = (
status: item.applicationStatus, status: item.applicationStatus,
description: item.description, description: item.description,
serverId: item.serverId, serverId: item.serverId,
serverName: item?.server?.name || null,
})) || []; })) || [];
const compose: Services[] = const compose: Services[] =
environment.compose?.map((item) => ({ environment.compose?.map((item) => {
appName: item.appName, // Get the most recent deployment date
name: item.name, let lastDeployDate: Date | null = null;
type: "compose", const deployments = (item as any).deployments;
id: item.composeId, if (deployments && deployments.length > 0) {
createdAt: item.createdAt, for (const deployment of deployments) {
status: item.composeStatus, const deployDate = new Date(
description: item.description, deployment.finishedAt ||
serverId: item.serverId, deployment.startedAt ||
})) || []; deployment.createdAt,
);
if (!lastDeployDate || deployDate > lastDeployDate) {
lastDeployDate = deployDate;
}
}
}
return {
appName: item.appName,
name: item.name,
type: "compose",
id: item.composeId,
createdAt: item.createdAt,
status: item.composeStatus,
description: item.description,
serverId: item.serverId,
serverName: item?.server?.name || null,
lastDeployDate,
};
}) || [];
allServices.push( allServices.push(
...applications, ...applications,
@@ -237,9 +281,9 @@ const EnvironmentPage = (
const { data: auth } = api.user.get.useQuery(); const { data: auth } = api.user.get.useQuery();
const [sortBy, setSortBy] = useState<string>(() => { const [sortBy, setSortBy] = useState<string>(() => {
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
return localStorage.getItem("servicesSort") || "createdAt-desc"; return localStorage.getItem("servicesSort") || "lastDeploy-desc";
} }
return "createdAt-desc"; return "lastDeploy-desc";
}); });
useEffect(() => { useEffect(() => {
@@ -261,10 +305,45 @@ const EnvironmentPage = (
comparison = comparison =
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
break; break;
case "lastDeploy": {
const aLastDeploy = a.lastDeployDate;
const bLastDeploy = b.lastDeployDate;
if (direction === "desc") {
// For "desc" (newest first): services with deployments first, then those without
if (!aLastDeploy && !bLastDeploy) {
comparison = 0;
} else if (!aLastDeploy) {
comparison = 1; // a (no deploy) goes after b (has deploy)
} else if (!bLastDeploy) {
comparison = -1; // a (has deploy) goes before b (no deploy)
} else {
// Both have deployments: newest first (negative if a is newer)
comparison = bLastDeploy.getTime() - aLastDeploy.getTime();
}
} else {
// For "asc" (oldest first): services with deployments first, then those without
if (!aLastDeploy && !bLastDeploy) {
comparison = 0;
} else if (!aLastDeploy) {
comparison = 1; // a (no deploy) goes after b (has deploy)
} else if (!bLastDeploy) {
comparison = -1; // a (has deploy) goes before b (no deploy)
} else {
// Both have deployments: oldest first
comparison = aLastDeploy.getTime() - bLastDeploy.getTime();
}
}
break;
}
default: default:
comparison = 0; comparison = 0;
} }
return direction === "asc" ? comparison : -comparison; // For other fields, apply direction normally
if (field !== "lastDeploy") {
return direction === "asc" ? comparison : -comparison;
}
return comparison;
}); });
}; };
@@ -320,6 +399,7 @@ const EnvironmentPage = (
const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false); const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false);
const [deleteVolumes, setDeleteVolumes] = useState(false); const [deleteVolumes, setDeleteVolumes] = useState(false);
const [selectedServerId, setSelectedServerId] = useState<string>("all");
const handleSelectAll = () => { const handleSelectAll = () => {
if (selectedServices.length === filteredServices.length) { if (selectedServices.length === filteredServices.length) {
@@ -709,6 +789,27 @@ const EnvironmentPage = (
setIsBulkActionLoading(false); setIsBulkActionLoading(false);
}; };
// Get unique servers from services
const availableServers = useMemo(() => {
if (!applications) return [];
const servers = new Map<string, { serverId: string; serverName: string }>();
applications.forEach((service) => {
if (service.serverId && service.serverName) {
servers.set(service.serverId, {
serverId: service.serverId,
serverName: service.serverName,
});
}
});
return Array.from(servers.values());
}, [applications]);
// Check if there are services without a server (Dokploy server)
const hasServicesWithoutServer = useMemo(() => {
if (!applications) return false;
return applications.some((service) => !service.serverId);
}, [applications]);
const filteredServices = useMemo(() => { const filteredServices = useMemo(() => {
if (!applications) return []; if (!applications) return [];
const filtered = applications.filter( const filtered = applications.filter(
@@ -717,10 +818,14 @@ const EnvironmentPage = (
service.description service.description
?.toLowerCase() ?.toLowerCase()
.includes(searchQuery.toLowerCase())) && .includes(searchQuery.toLowerCase())) &&
(selectedTypes.length === 0 || selectedTypes.includes(service.type)), (selectedTypes.length === 0 || selectedTypes.includes(service.type)) &&
(selectedServerId === "" ||
selectedServerId === "all" ||
(selectedServerId === "dokploy-server" && !service.serverId) ||
service.serverId === selectedServerId),
); );
return sortServices(filtered); return sortServices(filtered);
}, [applications, searchQuery, selectedTypes, sortBy]); }, [applications, searchQuery, selectedTypes, selectedServerId, sortBy]);
const selectedServicesWithRunningStatus = useMemo(() => { const selectedServicesWithRunningStatus = useMemo(() => {
return filteredServices.filter( return filteredServices.filter(
@@ -1217,6 +1322,9 @@ const EnvironmentPage = (
<SelectValue placeholder="Sort by..." /> <SelectValue placeholder="Sort by..." />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="lastDeploy-desc">
Recently deployed
</SelectItem>
<SelectItem value="createdAt-desc"> <SelectItem value="createdAt-desc">
Newest first Newest first
</SelectItem> </SelectItem>
@@ -1291,6 +1399,39 @@ const EnvironmentPage = (
</Command> </Command>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
{(availableServers.length > 0 ||
hasServicesWithoutServer) && (
<Select
value={selectedServerId || "all"}
onValueChange={setSelectedServerId}
>
<SelectTrigger className="lg:w-[200px]">
<SelectValue placeholder="Filter by server..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All servers</SelectItem>
{hasServicesWithoutServer && (
<SelectItem value="dokploy-server">
<div className="flex items-center gap-2">
<ServerIcon className="size-4" />
<span>Dokploy server</span>
</div>
</SelectItem>
)}
{availableServers.map((server) => (
<SelectItem
key={server.serverId}
value={server.serverId}
>
<div className="flex items-center gap-2">
<ServerIcon className="size-4" />
<span>{server.serverName}</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div> </div>
</div> </div>
@@ -1396,7 +1537,15 @@ const EnvironmentPage = (
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardFooter className="mt-auto"> <CardFooter className="mt-auto">
<div className="space-y-1 text-sm"> <div className="space-y-1 text-sm w-full">
{service.serverName && (
<div className="flex items-center gap-1.5 text-xs text-muted-foreground mb-1">
<ServerIcon className="size-3" />
<span className="truncate">
{service.serverName}
</span>
</div>
)}
<DateTooltip date={service.createdAt}> <DateTooltip date={service.createdAt}>
Created Created
</DateTooltip> </DateTooltip>
+3 -3
View File
@@ -1,6 +1,6 @@
import { findAdmin } from "@dokploy/server"; import { findAdmin } from "@dokploy/server";
import { db } from "@dokploy/server/db"; import { db } from "@dokploy/server/db";
import { users_temp } from "@dokploy/server/db/schema"; import { user } from "@dokploy/server/db/schema";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
(async () => { (async () => {
@@ -8,11 +8,11 @@ import { eq } from "drizzle-orm";
const result = await findAdmin(); const result = await findAdmin();
const update = await db const update = await db
.update(users_temp) .update(user)
.set({ .set({
twoFactorEnabled: false, twoFactorEnabled: false,
}) })
.where(eq(users_temp.id, result.userId)); .where(eq(user.id, result.userId));
if (update) { if (update) {
console.log("2FA reset successful"); console.log("2FA reset successful");
+9 -3
View File
@@ -3,13 +3,14 @@ import {
addNewService, addNewService,
checkServiceAccess, checkServiceAccess,
cloneCompose, cloneCompose,
cloneComposeRemote,
createCommand, createCommand,
createCompose, createCompose,
createComposeByTemplate, createComposeByTemplate,
createDomain, createDomain,
createMount, createMount,
deleteMount, deleteMount,
execAsync,
execAsyncRemote,
findComposeById, findComposeById,
findDomainsByComposeId, findDomainsByComposeId,
findEnvironmentById, findEnvironmentById,
@@ -245,6 +246,7 @@ export const composeRouter = createTRPCRouter({
}); });
} }
await cleanQueuesByCompose(input.composeId); await cleanQueuesByCompose(input.composeId);
return { success: true, message: "Queues cleaned successfully" };
}), }),
loadServices: protectedProcedure loadServices: protectedProcedure
@@ -301,10 +303,12 @@ export const composeRouter = createTRPCRouter({
message: "You are not authorized to fetch this compose", message: "You are not authorized to fetch this compose",
}); });
} }
const command = await cloneCompose(compose);
if (compose.serverId) { if (compose.serverId) {
await cloneComposeRemote(compose); await execAsyncRemote(compose.serverId, command);
} else { } else {
await cloneCompose(compose); await execAsync(command);
} }
return compose.sourceType; return compose.sourceType;
} catch (err) { } catch (err) {
@@ -405,6 +409,7 @@ export const composeRouter = createTRPCRouter({
removeOnFail: true, removeOnFail: true,
}, },
); );
return { success: true, message: "Deployment queued" };
}), }),
redeploy: protectedProcedure redeploy: protectedProcedure
.input(apiRedeployCompose) .input(apiRedeployCompose)
@@ -440,6 +445,7 @@ export const composeRouter = createTRPCRouter({
removeOnFail: true, removeOnFail: true,
}, },
); );
return { success: true, message: "Redeployment queued" };
}), }),
stop: protectedProcedure stop: protectedProcedure
.input(apiFindCompose) .input(apiFindCompose)
@@ -1,8 +1,8 @@
import { import {
createDiscordNotification, createDiscordNotification,
createEmailNotification, createEmailNotification,
createLarkNotification,
createGotifyNotification, createGotifyNotification,
createLarkNotification,
createNtfyNotification, createNtfyNotification,
createSlackNotification, createSlackNotification,
createTelegramNotification, createTelegramNotification,
@@ -11,16 +11,16 @@ import {
removeNotificationById, removeNotificationById,
sendDiscordNotification, sendDiscordNotification,
sendEmailNotification, sendEmailNotification,
sendLarkNotification,
sendGotifyNotification, sendGotifyNotification,
sendLarkNotification,
sendNtfyNotification, sendNtfyNotification,
sendServerThresholdNotifications, sendServerThresholdNotifications,
sendSlackNotification, sendSlackNotification,
sendTelegramNotification, sendTelegramNotification,
updateDiscordNotification, updateDiscordNotification,
updateEmailNotification, updateEmailNotification,
updateLarkNotification,
updateGotifyNotification, updateGotifyNotification,
updateLarkNotification,
updateNtfyNotification, updateNtfyNotification,
updateSlackNotification, updateSlackNotification,
updateTelegramNotification, updateTelegramNotification,
@@ -38,29 +38,29 @@ import { db } from "@/server/db";
import { import {
apiCreateDiscord, apiCreateDiscord,
apiCreateEmail, apiCreateEmail,
apiCreateLark,
apiCreateGotify, apiCreateGotify,
apiCreateLark,
apiCreateNtfy, apiCreateNtfy,
apiCreateSlack, apiCreateSlack,
apiCreateTelegram, apiCreateTelegram,
apiFindOneNotification, apiFindOneNotification,
apiTestDiscordConnection, apiTestDiscordConnection,
apiTestEmailConnection, apiTestEmailConnection,
apiTestLarkConnection,
apiTestGotifyConnection, apiTestGotifyConnection,
apiTestLarkConnection,
apiTestNtfyConnection, apiTestNtfyConnection,
apiTestSlackConnection, apiTestSlackConnection,
apiTestTelegramConnection, apiTestTelegramConnection,
apiUpdateDiscord, apiUpdateDiscord,
apiUpdateEmail, apiUpdateEmail,
apiUpdateLark,
apiUpdateGotify, apiUpdateGotify,
apiUpdateLark,
apiUpdateNtfy, apiUpdateNtfy,
apiUpdateSlack, apiUpdateSlack,
apiUpdateTelegram, apiUpdateTelegram,
notifications, notifications,
server, server,
users_temp, user,
} from "@/server/db/schema"; } from "@/server/db/schema";
export const notificationRouter = createTRPCRouter({ export const notificationRouter = createTRPCRouter({
@@ -359,9 +359,9 @@ export const notificationRouter = createTRPCRouter({
if (input.ServerType === "Dokploy") { if (input.ServerType === "Dokploy") {
const result = await db const result = await db
.select() .select()
.from(users_temp) .from(user)
.where( .where(
sql`${users_temp.metricsConfig}::jsonb -> 'server' ->> 'token' = ${input.Token}`, sql`${user.metricsConfig}::jsonb -> 'server' ->> 'token' = ${input.Token}`,
); );
if (!result?.[0]?.id) { if (!result?.[0]?.id) {
@@ -41,6 +41,11 @@ export const organizationRouter = createTRPCRouter({
}); });
} }
// Check if this is the user's first organization
const existingMemberships = await db.query.member.findMany({
where: eq(member.userId, ctx.user.id),
});
await db.insert(member).values({ await db.insert(member).values({
organizationId: result.id, organizationId: result.id,
role: "owner", role: "owner",
@@ -63,6 +68,11 @@ export const organizationRouter = createTRPCRouter({
), ),
), ),
), ),
with: {
members: {
where: eq(member.userId, ctx.user.id),
},
},
}); });
return memberResult; return memberResult;
}), }),
@@ -184,4 +194,45 @@ export const organizationRouter = createTRPCRouter({
.delete(invitation) .delete(invitation)
.where(eq(invitation.id, input.invitationId)); .where(eq(invitation.id, input.invitationId));
}), }),
setDefault: protectedProcedure
.input(
z.object({
organizationId: z.string().min(1),
}),
)
.mutation(async ({ ctx, input }) => {
// Verify user is a member of this organization
const userMember = await db.query.member.findFirst({
where: and(
eq(member.organizationId, input.organizationId),
eq(member.userId, ctx.user.id),
),
});
if (!userMember) {
throw new TRPCError({
code: "FORBIDDEN",
message: "You are not a member of this organization",
});
}
// First, unset all defaults for this user
await db
.update(member)
.set({ isDefault: false })
.where(eq(member.userId, ctx.user.id));
// Then set this organization as default
await db
.update(member)
.set({ isDefault: true })
.where(
and(
eq(member.organizationId, input.organizationId),
eq(member.userId, ctx.user.id),
),
);
return { success: true };
}),
}); });
@@ -383,6 +383,15 @@ export const serverRouter = createTRPCRouter({
const ip = await getPublicIpWithFallback(); const ip = await getPublicIpWithFallback();
return ip; return ip;
}), }),
getServerTime: protectedProcedure.query(() => {
if (IS_CLOUD) {
return null;
}
return {
time: new Date(),
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
};
}),
getServerMetrics: protectedProcedure getServerMetrics: protectedProcedure
.input( .input(
z.object({ z.object({
+2 -2
View File
@@ -4,6 +4,7 @@ import {
findNotificationById, findNotificationById,
findOrganizationById, findOrganizationById,
findUserById, findUserById,
getDokployUrl,
getUserByToken, getUserByToken,
IS_CLOUD, IS_CLOUD,
removeUserById, removeUserById,
@@ -419,11 +420,10 @@ export const userRouter = createTRPCRouter({
}); });
} }
const admin = await findAdmin();
const host = const host =
process.env.NODE_ENV === "development" process.env.NODE_ENV === "development"
? "http://localhost:3000" ? "http://localhost:3000"
: admin.user.host; : await getDokployUrl();
const inviteLink = `${host}/invitation?token=${input.invitationId}`; const inviteLink = `${host}/invitation?token=${input.invitationId}`;
const organization = await findOrganizationById( const organization = await findOrganizationById(
+32 -80
View File
@@ -2,13 +2,8 @@ import {
deployApplication, deployApplication,
deployCompose, deployCompose,
deployPreviewApplication, deployPreviewApplication,
deployRemoteApplication,
deployRemoteCompose,
deployRemotePreviewApplication,
rebuildApplication, rebuildApplication,
rebuildCompose, rebuildCompose,
rebuildRemoteApplication,
rebuildRemoteCompose,
updateApplicationStatus, updateApplicationStatus,
updateCompose, updateCompose,
updatePreviewDeployment, updatePreviewDeployment,
@@ -24,91 +19,48 @@ export const deploymentWorker = new Worker(
if (job.data.applicationType === "application") { if (job.data.applicationType === "application") {
await updateApplicationStatus(job.data.applicationId, "running"); await updateApplicationStatus(job.data.applicationId, "running");
if (job.data.server) { if (job.data.type === "redeploy") {
if (job.data.type === "redeploy") { await rebuildApplication({
await rebuildRemoteApplication({ applicationId: job.data.applicationId,
applicationId: job.data.applicationId, titleLog: job.data.titleLog,
titleLog: job.data.titleLog, descriptionLog: job.data.descriptionLog,
descriptionLog: job.data.descriptionLog, });
}); } else if (job.data.type === "deploy") {
} else if (job.data.type === "deploy") { await deployApplication({
await deployRemoteApplication({ applicationId: job.data.applicationId,
applicationId: job.data.applicationId, titleLog: job.data.titleLog,
titleLog: job.data.titleLog, descriptionLog: job.data.descriptionLog,
descriptionLog: job.data.descriptionLog, });
});
}
} else {
if (job.data.type === "redeploy") {
await rebuildApplication({
applicationId: job.data.applicationId,
titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog,
});
} else if (job.data.type === "deploy") {
await deployApplication({
applicationId: job.data.applicationId,
titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog,
});
}
} }
} else if (job.data.applicationType === "compose") { } else if (job.data.applicationType === "compose") {
await updateCompose(job.data.composeId, { await updateCompose(job.data.composeId, {
composeStatus: "running", composeStatus: "running",
}); });
if (job.data.type === "deploy") {
if (job.data.server) { await deployCompose({
if (job.data.type === "redeploy") { composeId: job.data.composeId,
await rebuildRemoteCompose({ titleLog: job.data.titleLog,
composeId: job.data.composeId, descriptionLog: job.data.descriptionLog,
titleLog: job.data.titleLog, });
descriptionLog: job.data.descriptionLog, } else if (job.data.type === "redeploy") {
}); await rebuildCompose({
} else if (job.data.type === "deploy") { composeId: job.data.composeId,
await deployRemoteCompose({ titleLog: job.data.titleLog,
composeId: job.data.composeId, descriptionLog: job.data.descriptionLog,
titleLog: job.data.titleLog, });
descriptionLog: job.data.descriptionLog,
});
}
} else {
if (job.data.type === "deploy") {
await deployCompose({
composeId: job.data.composeId,
titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog,
});
} else if (job.data.type === "redeploy") {
await rebuildCompose({
composeId: job.data.composeId,
titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog,
});
}
} }
} else if (job.data.applicationType === "application-preview") { } else if (job.data.applicationType === "application-preview") {
await updatePreviewDeployment(job.data.previewDeploymentId, { await updatePreviewDeployment(job.data.previewDeploymentId, {
previewStatus: "running", previewStatus: "running",
}); });
if (job.data.server) {
if (job.data.type === "deploy") { if (job.data.type === "deploy") {
await deployRemotePreviewApplication({ await deployPreviewApplication({
applicationId: job.data.applicationId, applicationId: job.data.applicationId,
titleLog: job.data.titleLog, titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog, descriptionLog: job.data.descriptionLog,
previewDeploymentId: job.data.previewDeploymentId, previewDeploymentId: job.data.previewDeploymentId,
}); });
}
} else {
if (job.data.type === "deploy") {
await deployPreviewApplication({
applicationId: job.data.applicationId,
titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog,
previewDeploymentId: job.data.previewDeploymentId,
});
}
} }
} }
} catch (error) { } catch (error) {
+16
View File
@@ -2,6 +2,7 @@ import type http from "node:http";
import { import {
docker, docker,
execAsync, execAsync,
getHostSystemStats,
getLastAdvancedStatsFile, getLastAdvancedStatsFile,
recordAdvancedStats, recordAdvancedStats,
validateRequest, validateRequest,
@@ -49,6 +50,21 @@ export const setupDockerStatsMonitoringSocketServer = (
} }
const intervalId = setInterval(async () => { const intervalId = setInterval(async () => {
try { try {
// Special case: when monitoring "dokploy", get host system stats instead of container stats
if (appName === "dokploy") {
const stat = await getHostSystemStats();
await recordAdvancedStats(stat, appName);
const data = await getLastAdvancedStatsFile(appName);
ws.send(
JSON.stringify({
data,
}),
);
return;
}
const filter = { const filter = {
status: ["running"], status: ["running"],
...(appType === "application" && { ...(appType === "application" && {
+1 -1
View File
@@ -22,7 +22,7 @@ import {
await initializeNetwork(); await initializeNetwork();
createDefaultTraefikConfig(); createDefaultTraefikConfig();
createDefaultServerTraefikConfig(); createDefaultServerTraefikConfig();
await execAsync("docker pull traefik:v3.5.0"); await execAsync("docker pull traefik:v3.6.1");
await initializeStandaloneTraefik(); await initializeStandaloneTraefik();
await initializeRedis(); await initializeRedis();
await initializePostgres(); await initializePostgres();
+3 -3
View File
@@ -6,7 +6,7 @@
// boolean, // boolean,
// } from "drizzle-orm/pg-core"; // } from "drizzle-orm/pg-core";
// export const users_temp = pgTable("users_temp", { // export const user = pgTable("user", {
// id: text("id").primaryKey(), // id: text("id").primaryKey(),
// name: text("name").notNull(), // name: text("name").notNull(),
// email: text("email").notNull().unique(), // email: text("email").notNull().unique(),
@@ -29,7 +29,7 @@
// userAgent: text("user_agent"), // userAgent: text("user_agent"),
// userId: text("user_id") // userId: text("user_id")
// .notNull() // .notNull()
// .references(() => users_temp.id, { onDelete: "cascade" }), // .references(() => user.id, { onDelete: "cascade" }),
// activeOrganizationId: text("active_organization_id"), // activeOrganizationId: text("active_organization_id"),
// }); // });
@@ -39,7 +39,7 @@
// providerId: text("provider_id").notNull(), // providerId: text("provider_id").notNull(),
// userId: text("user_id") // userId: text("user_id")
// .notNull() // .notNull()
// .references(() => users_temp.id, { onDelete: "cascade" }), // .references(() => user.id, { onDelete: "cascade" }),
// accessToken: text("access_token"), // accessToken: text("access_token"),
// refreshToken: text("refresh_token"), // refreshToken: text("refresh_token"),
// idToken: text("id_token"), // idToken: text("id_token"),
+1 -2
View File
@@ -61,7 +61,7 @@
"lodash": "4.17.21", "lodash": "4.17.21",
"micromatch": "4.0.8", "micromatch": "4.0.8",
"nanoid": "3.3.11", "nanoid": "3.3.11",
"node-os-utils": "1.3.7", "node-os-utils": "2.0.1",
"node-pty": "1.0.0", "node-pty": "1.0.0",
"node-schedule": "2.1.1", "node-schedule": "2.1.1",
"nodemailer": "6.9.14", "nodemailer": "6.9.14",
@@ -88,7 +88,6 @@
"@types/lodash": "4.17.4", "@types/lodash": "4.17.4",
"@types/micromatch": "4.0.9", "@types/micromatch": "4.0.9",
"@types/node": "^18.19.104", "@types/node": "^18.19.104",
"@types/node-os-utils": "1.3.4",
"@types/node-schedule": "2.1.6", "@types/node-schedule": "2.1.6",
"@types/nodemailer": "^6.4.17", "@types/nodemailer": "^6.4.17",
"@types/qrcode": "^1.5.5", "@types/qrcode": "^1.5.5",
+16 -15
View File
@@ -9,7 +9,7 @@ import {
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { projects } from "./project"; import { projects } from "./project";
import { server } from "./server"; import { server } from "./server";
import { users_temp } from "./user"; import { user } from "./user";
export const account = pgTable("account", { export const account = pgTable("account", {
id: text("id") id: text("id")
@@ -21,7 +21,7 @@ export const account = pgTable("account", {
providerId: text("provider_id").notNull(), providerId: text("provider_id").notNull(),
userId: text("user_id") userId: text("user_id")
.notNull() .notNull()
.references(() => users_temp.id, { onDelete: "cascade" }), .references(() => user.id, { onDelete: "cascade" }),
accessToken: text("access_token"), accessToken: text("access_token"),
refreshToken: text("refresh_token"), refreshToken: text("refresh_token"),
idToken: text("id_token"), idToken: text("id_token"),
@@ -39,9 +39,9 @@ export const account = pgTable("account", {
}); });
export const accountRelations = relations(account, ({ one }) => ({ export const accountRelations = relations(account, ({ one }) => ({
user: one(users_temp, { user: one(user, {
fields: [account.userId], fields: [account.userId],
references: [users_temp.id], references: [user.id],
}), }),
})); }));
@@ -65,15 +65,15 @@ export const organization = pgTable("organization", {
metadata: text("metadata"), metadata: text("metadata"),
ownerId: text("owner_id") ownerId: text("owner_id")
.notNull() .notNull()
.references(() => users_temp.id, { onDelete: "cascade" }), .references(() => user.id, { onDelete: "cascade" }),
}); });
export const organizationRelations = relations( export const organizationRelations = relations(
organization, organization,
({ one, many }) => ({ ({ one, many }) => ({
owner: one(users_temp, { owner: one(user, {
fields: [organization.ownerId], fields: [organization.ownerId],
references: [users_temp.id], references: [user.id],
}), }),
servers: many(server), servers: many(server),
projects: many(projects), projects: many(projects),
@@ -90,10 +90,11 @@ export const member = pgTable("member", {
.references(() => organization.id, { onDelete: "cascade" }), .references(() => organization.id, { onDelete: "cascade" }),
userId: text("user_id") userId: text("user_id")
.notNull() .notNull()
.references(() => users_temp.id, { onDelete: "cascade" }), .references(() => user.id, { onDelete: "cascade" }),
role: text("role").notNull().$type<"owner" | "member" | "admin">(), role: text("role").notNull().$type<"owner" | "member" | "admin">(),
createdAt: timestamp("created_at").notNull(), createdAt: timestamp("created_at").notNull(),
teamId: text("team_id"), teamId: text("team_id"),
isDefault: boolean("is_default").notNull().default(false),
// Permissions // Permissions
canCreateProjects: boolean("canCreateProjects").notNull().default(false), canCreateProjects: boolean("canCreateProjects").notNull().default(false),
canAccessToSSHKeys: boolean("canAccessToSSHKeys").notNull().default(false), canAccessToSSHKeys: boolean("canAccessToSSHKeys").notNull().default(false),
@@ -133,9 +134,9 @@ export const memberRelations = relations(member, ({ one }) => ({
fields: [member.organizationId], fields: [member.organizationId],
references: [organization.id], references: [organization.id],
}), }),
user: one(users_temp, { user: one(user, {
fields: [member.userId], fields: [member.userId],
references: [users_temp.id], references: [user.id],
}), }),
})); }));
@@ -150,7 +151,7 @@ export const invitation = pgTable("invitation", {
expiresAt: timestamp("expires_at").notNull(), expiresAt: timestamp("expires_at").notNull(),
inviterId: text("inviter_id") inviterId: text("inviter_id")
.notNull() .notNull()
.references(() => users_temp.id, { onDelete: "cascade" }), .references(() => user.id, { onDelete: "cascade" }),
teamId: text("team_id"), teamId: text("team_id"),
}); });
@@ -167,7 +168,7 @@ export const twoFactor = pgTable("two_factor", {
backupCodes: text("backup_codes").notNull(), backupCodes: text("backup_codes").notNull(),
userId: text("user_id") userId: text("user_id")
.notNull() .notNull()
.references(() => users_temp.id, { onDelete: "cascade" }), .references(() => user.id, { onDelete: "cascade" }),
}); });
export const apikey = pgTable("apikey", { export const apikey = pgTable("apikey", {
@@ -178,7 +179,7 @@ export const apikey = pgTable("apikey", {
key: text("key").notNull(), key: text("key").notNull(),
userId: text("user_id") userId: text("user_id")
.notNull() .notNull()
.references(() => users_temp.id, { onDelete: "cascade" }), .references(() => user.id, { onDelete: "cascade" }),
refillInterval: integer("refill_interval"), refillInterval: integer("refill_interval"),
refillAmount: integer("refill_amount"), refillAmount: integer("refill_amount"),
lastRefillAt: timestamp("last_refill_at"), lastRefillAt: timestamp("last_refill_at"),
@@ -197,8 +198,8 @@ export const apikey = pgTable("apikey", {
}); });
export const apikeyRelations = relations(apikey, ({ one }) => ({ export const apikeyRelations = relations(apikey, ({ one }) => ({
user: one(users_temp, { user: one(user, {
fields: [apikey.userId], fields: [apikey.userId],
references: [users_temp.id], references: [user.id],
}), }),
})); }));
@@ -28,6 +28,8 @@ import { server } from "./server";
import { import {
applicationStatus, applicationStatus,
certificateType, certificateType,
type EndpointSpecSwarm,
EndpointSpecSwarmSchema,
type HealthCheckSwarm, type HealthCheckSwarm,
HealthCheckSwarmSchema, HealthCheckSwarmSchema,
type LabelsSwarm, type LabelsSwarm,
@@ -167,6 +169,7 @@ export const applications = pgTable("application", {
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(), labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(), networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }), stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
endpointSpecSwarm: json("endpointSpecSwarm").$type<EndpointSpecSwarm>(),
// //
replicas: integer("replicas").default(1).notNull(), replicas: integer("replicas").default(1).notNull(),
applicationStatus: applicationStatus("applicationStatus") applicationStatus: applicationStatus("applicationStatus")
@@ -318,6 +321,7 @@ const createSchema = createInsertSchema(applications, {
previewLabels: z.array(z.string()).optional(), previewLabels: z.array(z.string()).optional(),
cleanCache: z.boolean().optional(), cleanCache: z.boolean().optional(),
stopGracePeriodSwarm: z.bigint().nullable(), stopGracePeriodSwarm: z.bigint().nullable(),
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
}); });
export const apiCreateApplication = createSchema.pick({ export const apiCreateApplication = createSchema.pick({
+4 -4
View File
@@ -19,7 +19,7 @@ import { mariadb } from "./mariadb";
import { mongo } from "./mongo"; import { mongo } from "./mongo";
import { mysql } from "./mysql"; import { mysql } from "./mysql";
import { postgres } from "./postgres"; import { postgres } from "./postgres";
import { users_temp } from "./user"; import { user } from "./user";
export const databaseType = pgEnum("databaseType", [ export const databaseType = pgEnum("databaseType", [
"postgres", "postgres",
"mariadb", "mariadb",
@@ -74,7 +74,7 @@ export const backups = pgTable("backup", {
mongoId: text("mongoId").references((): AnyPgColumn => mongo.mongoId, { mongoId: text("mongoId").references((): AnyPgColumn => mongo.mongoId, {
onDelete: "cascade", onDelete: "cascade",
}), }),
userId: text("userId").references(() => users_temp.id), userId: text("userId").references(() => user.id),
// Only for compose backups // Only for compose backups
metadata: jsonb("metadata").$type< metadata: jsonb("metadata").$type<
| { | {
@@ -118,9 +118,9 @@ export const backupsRelations = relations(backups, ({ one, many }) => ({
fields: [backups.mongoId], fields: [backups.mongoId],
references: [mongo.mongoId], references: [mongo.mongoId],
}), }),
user: one(users_temp, { user: one(user, {
fields: [backups.userId], fields: [backups.userId],
references: [users_temp.id], references: [user.id],
}), }),
compose: one(compose, { compose: one(compose, {
fields: [backups.composeId], fields: [backups.composeId],
@@ -8,7 +8,7 @@ import { bitbucket } from "./bitbucket";
import { gitea } from "./gitea"; import { gitea } from "./gitea";
import { github } from "./github"; import { github } from "./github";
import { gitlab } from "./gitlab"; import { gitlab } from "./gitlab";
import { users_temp } from "./user"; import { user } from "./user";
export const gitProviderType = pgEnum("gitProviderType", [ export const gitProviderType = pgEnum("gitProviderType", [
"github", "github",
@@ -32,7 +32,7 @@ export const gitProvider = pgTable("git_provider", {
.references(() => organization.id, { onDelete: "cascade" }), .references(() => organization.id, { onDelete: "cascade" }),
userId: text("userId") userId: text("userId")
.notNull() .notNull()
.references(() => users_temp.id, { onDelete: "cascade" }), .references(() => user.id, { onDelete: "cascade" }),
}); });
export const gitProviderRelations = relations(gitProvider, ({ one }) => ({ export const gitProviderRelations = relations(gitProvider, ({ one }) => ({
@@ -56,9 +56,9 @@ export const gitProviderRelations = relations(gitProvider, ({ one }) => ({
fields: [gitProvider.organizationId], fields: [gitProvider.organizationId],
references: [organization.id], references: [organization.id],
}), }),
user: one(users_temp, { user: one(user, {
fields: [gitProvider.userId], fields: [gitProvider.userId],
references: [users_temp.id], references: [user.id],
}), }),
})); }));
+4
View File
@@ -9,6 +9,8 @@ import { mounts } from "./mount";
import { server } from "./server"; import { server } from "./server";
import { import {
applicationStatus, applicationStatus,
type EndpointSpecSwarm,
EndpointSpecSwarmSchema,
type HealthCheckSwarm, type HealthCheckSwarm,
HealthCheckSwarmSchema, HealthCheckSwarmSchema,
type LabelsSwarm, type LabelsSwarm,
@@ -63,6 +65,7 @@ export const mariadb = pgTable("mariadb", {
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(), labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(), networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }), stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
endpointSpecSwarm: json("endpointSpecSwarm").$type<EndpointSpecSwarm>(),
replicas: integer("replicas").default(1).notNull(), replicas: integer("replicas").default(1).notNull(),
createdAt: text("createdAt") createdAt: text("createdAt")
.notNull() .notNull()
@@ -130,6 +133,7 @@ const createSchema = createInsertSchema(mariadb, {
labelsSwarm: LabelsSwarmSchema.nullable(), labelsSwarm: LabelsSwarmSchema.nullable(),
networkSwarm: NetworkSwarmSchema.nullable(), networkSwarm: NetworkSwarmSchema.nullable(),
stopGracePeriodSwarm: z.bigint().nullable(), stopGracePeriodSwarm: z.bigint().nullable(),
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
}); });
export const apiCreateMariaDB = createSchema export const apiCreateMariaDB = createSchema
+4
View File
@@ -16,6 +16,8 @@ import { mounts } from "./mount";
import { server } from "./server"; import { server } from "./server";
import { import {
applicationStatus, applicationStatus,
type EndpointSpecSwarm,
EndpointSpecSwarmSchema,
type HealthCheckSwarm, type HealthCheckSwarm,
HealthCheckSwarmSchema, HealthCheckSwarmSchema,
type LabelsSwarm, type LabelsSwarm,
@@ -66,6 +68,7 @@ export const mongo = pgTable("mongo", {
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(), labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(), networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }), stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
endpointSpecSwarm: json("endpointSpecSwarm").$type<EndpointSpecSwarm>(),
replicas: integer("replicas").default(1).notNull(), replicas: integer("replicas").default(1).notNull(),
createdAt: text("createdAt") createdAt: text("createdAt")
.notNull() .notNull()
@@ -127,6 +130,7 @@ const createSchema = createInsertSchema(mongo, {
labelsSwarm: LabelsSwarmSchema.nullable(), labelsSwarm: LabelsSwarmSchema.nullable(),
networkSwarm: NetworkSwarmSchema.nullable(), networkSwarm: NetworkSwarmSchema.nullable(),
stopGracePeriodSwarm: z.bigint().nullable(), stopGracePeriodSwarm: z.bigint().nullable(),
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
}); });
export const apiCreateMongo = createSchema export const apiCreateMongo = createSchema
+4
View File
@@ -9,6 +9,8 @@ import { mounts } from "./mount";
import { server } from "./server"; import { server } from "./server";
import { import {
applicationStatus, applicationStatus,
type EndpointSpecSwarm,
EndpointSpecSwarmSchema,
type HealthCheckSwarm, type HealthCheckSwarm,
HealthCheckSwarmSchema, HealthCheckSwarmSchema,
type LabelsSwarm, type LabelsSwarm,
@@ -61,6 +63,7 @@ export const mysql = pgTable("mysql", {
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(), labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(), networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }), stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
endpointSpecSwarm: json("endpointSpecSwarm").$type<EndpointSpecSwarm>(),
replicas: integer("replicas").default(1).notNull(), replicas: integer("replicas").default(1).notNull(),
createdAt: text("createdAt") createdAt: text("createdAt")
.notNull() .notNull()
@@ -127,6 +130,7 @@ const createSchema = createInsertSchema(mysql, {
labelsSwarm: LabelsSwarmSchema.nullable(), labelsSwarm: LabelsSwarmSchema.nullable(),
networkSwarm: NetworkSwarmSchema.nullable(), networkSwarm: NetworkSwarmSchema.nullable(),
stopGracePeriodSwarm: z.bigint().nullable(), stopGracePeriodSwarm: z.bigint().nullable(),
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
}); });
export const apiCreateMySql = createSchema export const apiCreateMySql = createSchema
@@ -9,6 +9,8 @@ import { mounts } from "./mount";
import { server } from "./server"; import { server } from "./server";
import { import {
applicationStatus, applicationStatus,
type EndpointSpecSwarm,
EndpointSpecSwarmSchema,
type HealthCheckSwarm, type HealthCheckSwarm,
HealthCheckSwarmSchema, HealthCheckSwarmSchema,
type LabelsSwarm, type LabelsSwarm,
@@ -61,6 +63,7 @@ export const postgres = pgTable("postgres", {
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(), labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(), networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }), stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
endpointSpecSwarm: json("endpointSpecSwarm").$type<EndpointSpecSwarm>(),
replicas: integer("replicas").default(1).notNull(), replicas: integer("replicas").default(1).notNull(),
createdAt: text("createdAt") createdAt: text("createdAt")
.notNull() .notNull()
@@ -120,6 +123,7 @@ const createSchema = createInsertSchema(postgres, {
labelsSwarm: LabelsSwarmSchema.nullable(), labelsSwarm: LabelsSwarmSchema.nullable(),
networkSwarm: NetworkSwarmSchema.nullable(), networkSwarm: NetworkSwarmSchema.nullable(),
stopGracePeriodSwarm: z.bigint().nullable(), stopGracePeriodSwarm: z.bigint().nullable(),
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
}); });
export const apiCreatePostgres = createSchema export const apiCreatePostgres = createSchema
+4 -1
View File
@@ -5,10 +5,11 @@ import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
import { environments } from "./environment"; import { environments } from "./environment";
import { mounts } from "./mount"; import { mounts } from "./mount";
import { projects } from "./project";
import { server } from "./server"; import { server } from "./server";
import { import {
applicationStatus, applicationStatus,
type EndpointSpecSwarm,
EndpointSpecSwarmSchema,
type HealthCheckSwarm, type HealthCheckSwarm,
HealthCheckSwarmSchema, HealthCheckSwarmSchema,
type LabelsSwarm, type LabelsSwarm,
@@ -61,6 +62,7 @@ export const redis = pgTable("redis", {
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(), labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(), networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }), stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
endpointSpecSwarm: json("endpointSpecSwarm").$type<EndpointSpecSwarm>(),
replicas: integer("replicas").default(1).notNull(), replicas: integer("replicas").default(1).notNull(),
environmentId: text("environmentId") environmentId: text("environmentId")
@@ -110,6 +112,7 @@ const createSchema = createInsertSchema(redis, {
labelsSwarm: LabelsSwarmSchema.nullable(), labelsSwarm: LabelsSwarmSchema.nullable(),
networkSwarm: NetworkSwarmSchema.nullable(), networkSwarm: NetworkSwarmSchema.nullable(),
stopGracePeriodSwarm: z.bigint().nullable(), stopGracePeriodSwarm: z.bigint().nullable(),
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
}); });
export const apiCreateRedis = createSchema export const apiCreateRedis = createSchema
+4 -4
View File
@@ -7,7 +7,7 @@ import { applications } from "./application";
import { compose } from "./compose"; import { compose } from "./compose";
import { deployments } from "./deployment"; import { deployments } from "./deployment";
import { server } from "./server"; import { server } from "./server";
import { users_temp } from "./user"; import { user } from "./user";
import { generateAppName } from "./utils"; import { generateAppName } from "./utils";
export const shellTypes = pgEnum("shellType", ["bash", "sh"]); export const shellTypes = pgEnum("shellType", ["bash", "sh"]);
@@ -45,7 +45,7 @@ export const schedules = pgTable("schedule", {
serverId: text("serverId").references(() => server.serverId, { serverId: text("serverId").references(() => server.serverId, {
onDelete: "cascade", onDelete: "cascade",
}), }),
userId: text("userId").references(() => users_temp.id, { userId: text("userId").references(() => user.id, {
onDelete: "cascade", onDelete: "cascade",
}), }),
enabled: boolean("enabled").notNull().default(true), enabled: boolean("enabled").notNull().default(true),
@@ -69,9 +69,9 @@ export const schedulesRelations = relations(schedules, ({ one, many }) => ({
fields: [schedules.serverId], fields: [schedules.serverId],
references: [server.serverId], references: [server.serverId],
}), }),
user: one(users_temp, { user: one(user, {
fields: [schedules.userId], fields: [schedules.userId],
references: [users_temp.id], references: [user.id],
}), }),
deployments: many(deployments), deployments: many(deployments),
})); }));
+2 -2
View File
@@ -1,5 +1,5 @@
import { pgTable, text, timestamp } from "drizzle-orm/pg-core"; import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
import { users_temp } from "./user"; import { user } from "./user";
// OLD TABLE // OLD TABLE
export const session = pgTable("session_temp", { export const session = pgTable("session_temp", {
@@ -12,7 +12,7 @@ export const session = pgTable("session_temp", {
userAgent: text("user_agent"), userAgent: text("user_agent"),
userId: text("user_id") userId: text("user_id")
.notNull() .notNull()
.references(() => users_temp.id, { onDelete: "cascade" }), .references(() => user.id, { onDelete: "cascade" }),
impersonatedBy: text("impersonated_by"), impersonatedBy: text("impersonated_by"),
activeOrganizationId: text("active_organization_id"), activeOrganizationId: text("active_organization_id"),
}); });
+28
View File
@@ -74,6 +74,18 @@ export interface LabelsSwarm {
[name: string]: string; [name: string]: string;
} }
export interface EndpointPortConfigSwarm {
Protocol?: string | undefined;
TargetPort?: number | undefined;
PublishedPort?: number | undefined;
PublishMode?: string | undefined;
}
export interface EndpointSpecSwarm {
Mode?: string | undefined;
Ports?: EndpointPortConfigSwarm[] | undefined;
}
export const HealthCheckSwarmSchema = z export const HealthCheckSwarmSchema = z
.object({ .object({
Test: z.array(z.string()).optional(), Test: z.array(z.string()).optional(),
@@ -161,3 +173,19 @@ export const NetworkSwarmSchema = z.array(
); );
export const LabelsSwarmSchema = z.record(z.string()); export const LabelsSwarmSchema = z.record(z.string());
export const EndpointPortConfigSwarmSchema = z
.object({
Protocol: z.string().optional(),
TargetPort: z.number().optional(),
PublishedPort: z.number().optional(),
PublishMode: z.string().optional(),
})
.strict();
export const EndpointSpecSwarmSchema = z
.object({
Mode: z.string().optional(),
Ports: z.array(EndpointPortConfigSwarmSchema).optional(),
})
.strict();
+4 -4
View File
@@ -26,7 +26,7 @@ import { certificateType } from "./shared";
// OLD TABLE // OLD TABLE
// TEMP // TEMP
export const users_temp = pgTable("user_temp", { export const user = pgTable("user", {
id: text("id") id: text("id")
.notNull() .notNull()
.primaryKey() .primaryKey()
@@ -122,9 +122,9 @@ export const users_temp = pgTable("user_temp", {
serversQuantity: integer("serversQuantity").notNull().default(0), serversQuantity: integer("serversQuantity").notNull().default(0),
}); });
export const usersRelations = relations(users_temp, ({ one, many }) => ({ export const usersRelations = relations(user, ({ one, many }) => ({
account: one(account, { account: one(account, {
fields: [users_temp.id], fields: [user.id],
references: [account.userId], references: [account.userId],
}), }),
organizations: many(organization), organizations: many(organization),
@@ -134,7 +134,7 @@ export const usersRelations = relations(users_temp, ({ one, many }) => ({
schedules: many(schedules), schedules: many(schedules),
})); }));
const createSchema = createInsertSchema(users_temp, { const createSchema = createInsertSchema(user, {
id: z.string().min(1), id: z.string().min(1),
isRegistered: z.boolean().optional(), isRegistered: z.boolean().optional(),
}).omit({ }).omit({
+8 -2
View File
@@ -165,6 +165,7 @@ const { handler, api } = betterAuth({
organizationId: organization?.id || "", organizationId: organization?.id || "",
role: "owner", role: "owner",
createdAt: new Date(), createdAt: new Date(),
isDefault: true, // Mark first organization as default
}); });
}); });
} }
@@ -174,9 +175,14 @@ const { handler, api } = betterAuth({
session: { session: {
create: { create: {
before: async (session) => { before: async (session) => {
// Find the default organization for this user
// Priority: 1) isDefault=true, 2) most recently created
const member = await db.query.member.findFirst({ const member = await db.query.member.findFirst({
where: eq(schema.member.userId, session.userId), where: eq(schema.member.userId, session.userId),
orderBy: desc(schema.member.createdAt), orderBy: [
desc(schema.member.isDefault),
desc(schema.member.createdAt),
],
with: { with: {
organization: true, organization: true,
}, },
@@ -197,7 +203,7 @@ const { handler, api } = betterAuth({
updateAge: 60 * 60 * 24, updateAge: 60 * 60 * 24,
}, },
user: { user: {
modelName: "users_temp", modelName: "user",
additionalFields: { additionalFields: {
role: { role: {
type: "string", type: "string",
+112 -12
View File
@@ -1,5 +1,5 @@
import { promises } from "node:fs"; import { promises } from "node:fs";
import osUtils from "node-os-utils"; import { OSUtils } from "node-os-utils";
import { paths } from "../constants"; import { paths } from "../constants";
export interface Container { export interface Container {
@@ -38,22 +38,122 @@ export const recordAdvancedStats = async (
}); });
if (appName === "dokploy") { if (appName === "dokploy") {
const disk = await osUtils.drive.info("/"); const osutils = new OSUtils();
const diskResult = await osutils.disk.usageByMountPoint("/");
const diskUsage = disk.usedGb; if (diskResult.success && diskResult.data) {
const diskTotal = disk.totalGb; const disk = diskResult.data;
const diskUsedPercentage = disk.usedPercentage; const diskUsage = disk.used.toGB().toFixed(2);
const diskFree = disk.freeGb; const diskTotal = disk.total.toGB().toFixed(2);
const diskUsedPercentage = disk.usagePercentage;
const diskFree = disk.available.toGB().toFixed(2);
await updateStatsFile(appName, "disk", { await updateStatsFile(appName, "disk", {
diskTotal: +diskTotal, diskTotal: +diskTotal,
diskUsedPercentage: +diskUsedPercentage, diskUsedPercentage: +diskUsedPercentage,
diskUsage: +diskUsage, diskUsage: +diskUsage,
diskFree: +diskFree, diskFree: +diskFree,
}); });
}
} }
}; };
/**
* Get host system statistics using node-os-utils
* This is used when monitoring "dokploy" to show host stats instead of container stats
*/
export const getHostSystemStats = async (): Promise<Container> => {
const osutils = new OSUtils({
disk: {
includeStats: true, // Enable disk I/O statistics
},
});
// Get CPU usage
const cpuResult = await osutils.cpu.usage();
const cpuUsage = cpuResult.success ? cpuResult.data : 0;
// Get memory info
const memResult = await osutils.memory.info();
let memUsedGB = 0;
let memTotalGB = 0;
let memUsedPercent = 0;
if (memResult.success) {
memTotalGB = memResult.data.total.toGB();
memUsedGB = memResult.data.used.toGB();
memUsedPercent = memResult.data.usagePercentage;
}
// Get network stats from network.overview()
let netInputBytes = 0;
let netOutputBytes = 0;
const networkOverview = await osutils.network.overview();
if (networkOverview.success) {
netInputBytes = networkOverview.data.totalRxBytes.toBytes();
netOutputBytes = networkOverview.data.totalTxBytes.toBytes();
}
// Get Block I/O from disk.stats()
let blockReadBytes = 0;
let blockWriteBytes = 0;
const diskStats = await osutils.disk.stats();
if (diskStats.success && diskStats.data.length > 0) {
// Filter out virtual devices (loop, ram, sr, etc.) - only include real disk devices
const excludePatterns = [/^loop/, /^ram/, /^sr\d+$/, /^fd\d+$/];
for (const stat of diskStats.data) {
// Skip virtual devices
if (
stat.device &&
excludePatterns.some((pattern) => pattern.test(stat.device))
) {
continue;
}
// readBytes and writeBytes are DataSize objects with .toBytes() method
blockReadBytes += stat.readBytes.toBytes();
blockWriteBytes += stat.writeBytes.toBytes();
}
}
// Format values similar to docker stats
const formatBytes = (bytes: number): string => {
if (bytes >= 1024 * 1024 * 1024) {
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)}GiB`;
}
if (bytes >= 1024 * 1024) {
return `${(bytes / (1024 * 1024)).toFixed(2)}MiB`;
}
if (bytes >= 1024) {
return `${(bytes / 1024).toFixed(2)}KiB`;
}
return `${bytes}B`;
};
// Format memory usage similar to docker stats format: "used / total"
const memUsedFormatted = `${memUsedGB.toFixed(2)}GiB`;
const memTotalFormatted = `${memTotalGB.toFixed(2)}GiB`;
const memUsageFormatted = `${memUsedFormatted} / ${memTotalFormatted}`;
// Format network I/O
const netInputMb = netInputBytes / (1024 * 1024);
const netOutputMb = netOutputBytes / (1024 * 1024);
const netIOFormatted = `${netInputMb.toFixed(2)}MB / ${netOutputMb.toFixed(2)}MB`;
// Format Block I/O
const blockIOFormatted = `${formatBytes(blockReadBytes)} / ${formatBytes(blockWriteBytes)}`;
// Create a stat object compatible with recordAdvancedStats
return {
CPUPerc: `${cpuUsage.toFixed(2)}%`,
MemPerc: `${memUsedPercent.toFixed(2)}%`,
MemUsage: memUsageFormatted,
BlockIO: blockIOFormatted,
NetIO: netIOFormatted,
Container: "dokploy",
ID: "host-system",
Name: "dokploy",
};
};
export const getAdvancedStats = async (appName: string) => { export const getAdvancedStats = async (appName: string) => {
return { return {
cpu: await readStatsFile(appName, "cpu"), cpu: await readStatsFile(appName, "cpu"),
+15 -14
View File
@@ -3,26 +3,26 @@ import {
invitation, invitation,
member, member,
organization, organization,
users_temp, user,
} from "@dokploy/server/db/schema"; } from "@dokploy/server/db/schema";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { IS_CLOUD } from "../constants"; import { IS_CLOUD } from "../constants";
export const findUserById = async (userId: string) => { export const findUserById = async (userId: string) => {
const user = await db.query.users_temp.findFirst({ const userResult = await db.query.user.findFirst({
where: eq(users_temp.id, userId), where: eq(user.id, userId),
// with: { // with: {
// account: true, // account: true,
// }, // },
}); });
if (!user) { if (!userResult) {
throw new TRPCError({ throw new TRPCError({
code: "NOT_FOUND", code: "NOT_FOUND",
message: "User not found", message: "User not found",
}); });
} }
return user; return userResult;
}; };
export const findOrganizationById = async (organizationId: string) => { export const findOrganizationById = async (organizationId: string) => {
@@ -64,7 +64,7 @@ export const findAdmin = async () => {
}; };
export const getUserByToken = async (token: string) => { export const getUserByToken = async (token: string) => {
const user = await db.query.invitation.findFirst({ const userResult = await db.query.invitation.findFirst({
where: eq(invitation.id, token), where: eq(invitation.id, token),
columns: { columns: {
id: true, id: true,
@@ -76,29 +76,29 @@ export const getUserByToken = async (token: string) => {
}, },
}); });
if (!user) { if (!userResult) {
throw new TRPCError({ throw new TRPCError({
code: "NOT_FOUND", code: "NOT_FOUND",
message: "Invitation not found", message: "Invitation not found",
}); });
} }
const userAlreadyExists = await db.query.users_temp.findFirst({ const userAlreadyExists = await db.query.user.findFirst({
where: eq(users_temp.email, user?.email || ""), where: eq(user.email, userResult?.email || ""),
}); });
const { expiresAt, ...rest } = user; const { expiresAt, ...rest } = userResult;
return { return {
...rest, ...rest,
isExpired: user.expiresAt < new Date(), isExpired: userResult.expiresAt < new Date(),
userAlreadyExists: !!userAlreadyExists, userAlreadyExists: !!userAlreadyExists,
}; };
}; };
export const removeUserById = async (userId: string) => { export const removeUserById = async (userId: string) => {
await db await db
.delete(users_temp) .delete(user)
.where(eq(users_temp.id, userId)) .where(eq(user.id, userId))
.returning() .returning()
.then((res) => res[0]); .then((res) => res[0]);
}; };
@@ -110,7 +110,8 @@ export const getDokployUrl = async () => {
const admin = await findAdmin(); const admin = await findAdmin();
if (admin.user.host) { if (admin.user.host) {
return `https://${admin.user.host}`; const protocol = admin.user.https ? "https" : "http";
return `${protocol}://${admin.user.host}`;
} }
return `http://${admin.user.serverIp}:${process.env.PORT}`; return `http://${admin.user.serverIp}:${process.env.PORT}`;
}; };
+68 -323
View File
@@ -7,45 +7,32 @@ import {
} from "@dokploy/server/db/schema"; } from "@dokploy/server/db/schema";
import { getAdvancedStats } from "@dokploy/server/monitoring/utils"; import { getAdvancedStats } from "@dokploy/server/monitoring/utils";
import { import {
buildApplication,
getBuildCommand, getBuildCommand,
mechanizeDockerContainer, mechanizeDockerContainer,
} from "@dokploy/server/utils/builders"; } from "@dokploy/server/utils/builders";
import { sendBuildErrorNotifications } from "@dokploy/server/utils/notifications/build-error"; import { sendBuildErrorNotifications } from "@dokploy/server/utils/notifications/build-error";
import { sendBuildSuccessNotifications } from "@dokploy/server/utils/notifications/build-success"; import { sendBuildSuccessNotifications } from "@dokploy/server/utils/notifications/build-success";
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
import { import {
cloneBitbucketRepository, execAsync,
getBitbucketCloneCommand, execAsyncRemote,
} from "@dokploy/server/utils/providers/bitbucket"; } from "@dokploy/server/utils/process/execAsync";
import { import { cloneBitbucketRepository } from "@dokploy/server/utils/providers/bitbucket";
buildDocker, import { buildRemoteDocker } from "@dokploy/server/utils/providers/docker";
buildRemoteDocker,
} from "@dokploy/server/utils/providers/docker";
import { import {
cloneGitRepository, cloneGitRepository,
getCustomGitCloneCommand, getGitCommitInfo,
} from "@dokploy/server/utils/providers/git"; } from "@dokploy/server/utils/providers/git";
import { import { cloneGiteaRepository } from "@dokploy/server/utils/providers/gitea";
cloneGiteaRepository, import { cloneGithubRepository } from "@dokploy/server/utils/providers/github";
getGiteaCloneCommand, import { cloneGitlabRepository } from "@dokploy/server/utils/providers/gitlab";
} from "@dokploy/server/utils/providers/gitea";
import {
cloneGithubRepository,
getGithubCloneCommand,
} from "@dokploy/server/utils/providers/github";
import {
cloneGitlabRepository,
getGitlabCloneCommand,
} from "@dokploy/server/utils/providers/gitlab";
import { createTraefikConfig } from "@dokploy/server/utils/traefik/application"; import { createTraefikConfig } from "@dokploy/server/utils/traefik/application";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { encodeBase64 } from "../utils/docker/utils";
import { getDokployUrl } from "./admin"; import { getDokployUrl } from "./admin";
import { import {
createDeployment, createDeployment,
createDeploymentPreview, createDeploymentPreview,
updateDeployment,
updateDeploymentStatus, updateDeploymentStatus,
} from "./deployment"; } from "./deployment";
import { type Domain, getDomainHost } from "./domain"; import { type Domain, getDomainHost } from "./domain";
@@ -192,30 +179,31 @@ export const deployApplication = async ({
}); });
try { try {
let command = "set -e;";
if (application.sourceType === "github") { if (application.sourceType === "github") {
await cloneGithubRepository({ command += await cloneGithubRepository(application);
...application,
logPath: deployment.logPath,
});
await buildApplication(application, deployment.logPath);
} else if (application.sourceType === "gitlab") { } else if (application.sourceType === "gitlab") {
await cloneGitlabRepository(application, deployment.logPath); command += await cloneGitlabRepository(application);
await buildApplication(application, deployment.logPath);
} else if (application.sourceType === "gitea") { } else if (application.sourceType === "gitea") {
await cloneGiteaRepository(application, deployment.logPath); command += await cloneGiteaRepository(application);
await buildApplication(application, deployment.logPath);
} else if (application.sourceType === "bitbucket") { } else if (application.sourceType === "bitbucket") {
await cloneBitbucketRepository(application, deployment.logPath); command += await cloneBitbucketRepository(application);
await buildApplication(application, deployment.logPath);
} else if (application.sourceType === "docker") {
await buildDocker(application, deployment.logPath);
} else if (application.sourceType === "git") { } else if (application.sourceType === "git") {
await cloneGitRepository(application, deployment.logPath); command += await cloneGitRepository(application);
await buildApplication(application, deployment.logPath); } else if (application.sourceType === "docker") {
} else if (application.sourceType === "drop") { command += await buildRemoteDocker(application);
await buildApplication(application, deployment.logPath);
} }
command += getBuildCommand(application);
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
if (application.serverId) {
await execAsyncRemote(application.serverId, commandWithLog);
} else {
await execAsync(commandWithLog);
}
await mechanizeDockerContainer(application);
await updateDeploymentStatus(deployment.deploymentId, "done"); await updateDeploymentStatus(deployment.deploymentId, "done");
await updateApplicationStatus(applicationId, "done"); await updateApplicationStatus(applicationId, "done");
@@ -239,6 +227,12 @@ export const deployApplication = async ({
domains: application.domains, domains: application.domains,
}); });
} catch (error) { } catch (error) {
const command = `echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath};`;
if (application.serverId) {
await execAsyncRemote(application.serverId, command);
} else {
await execAsync(command);
}
await updateDeploymentStatus(deployment.deploymentId, "error"); await updateDeploymentStatus(deployment.deploymentId, "error");
await updateApplicationStatus(applicationId, "error"); await updateApplicationStatus(applicationId, "error");
@@ -253,8 +247,19 @@ export const deployApplication = async ({
}); });
throw error; throw error;
} } finally {
// Only extract commit info for non-docker sources
if (application.sourceType !== "docker") {
const commitInfo = await getGitCommitInfo(application);
if (commitInfo) {
await updateDeployment(deployment.deploymentId, {
title: commitInfo.message,
description: `Commit: ${commitInfo.hash}`,
});
}
}
}
return true; return true;
}; };
@@ -276,129 +281,21 @@ export const rebuildApplication = async ({
}); });
try { try {
if (application.sourceType === "github") { let command = "set -e;";
await buildApplication(application, deployment.logPath); // Check case for docker only
} else if (application.sourceType === "gitlab") { command += getBuildCommand(application);
await buildApplication(application, deployment.logPath); const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
} else if (application.sourceType === "bitbucket") {
await buildApplication(application, deployment.logPath);
} else if (application.sourceType === "docker") {
await buildDocker(application, deployment.logPath);
} else if (application.sourceType === "git") {
await buildApplication(application, deployment.logPath);
} else if (application.sourceType === "drop") {
await buildApplication(application, deployment.logPath);
}
await updateDeploymentStatus(deployment.deploymentId, "done");
await updateApplicationStatus(applicationId, "done");
} catch (error) {
await updateDeploymentStatus(deployment.deploymentId, "error");
await updateApplicationStatus(applicationId, "error");
throw error;
}
return true;
};
export const deployRemoteApplication = async ({
applicationId,
titleLog = "Manual deployment",
descriptionLog = "",
}: {
applicationId: string;
titleLog: string;
descriptionLog: string;
}) => {
const application = await findApplicationById(applicationId);
const buildLink = `${await getDokployUrl()}/dashboard/project/${application.environment.projectId}/environment/${application.environmentId}/services/application/${application.applicationId}?tab=deployments`;
const deployment = await createDeployment({
applicationId: applicationId,
title: titleLog,
description: descriptionLog,
});
try {
if (application.serverId) { if (application.serverId) {
let command = "set -e;"; await execAsyncRemote(application.serverId, commandWithLog);
if (application.sourceType === "github") { } else {
command += await getGithubCloneCommand({ await execAsync(commandWithLog);
...application,
serverId: application.serverId,
logPath: deployment.logPath,
});
} else if (application.sourceType === "gitlab") {
command += await getGitlabCloneCommand(application, deployment.logPath);
} else if (application.sourceType === "bitbucket") {
command += await getBitbucketCloneCommand(
application,
deployment.logPath,
);
} else if (application.sourceType === "gitea") {
command += await getGiteaCloneCommand(application, deployment.logPath);
} else if (application.sourceType === "git") {
command += await getCustomGitCloneCommand(
application,
deployment.logPath,
);
} else if (application.sourceType === "docker") {
command += await buildRemoteDocker(application, deployment.logPath);
}
if (application.sourceType !== "docker") {
command += getBuildCommand(application, deployment.logPath);
}
await execAsyncRemote(application.serverId, command);
await mechanizeDockerContainer(application);
} }
await mechanizeDockerContainer(application);
await updateDeploymentStatus(deployment.deploymentId, "done"); await updateDeploymentStatus(deployment.deploymentId, "done");
await updateApplicationStatus(applicationId, "done"); await updateApplicationStatus(applicationId, "done");
if (application.rollbackActive) {
const tagImage =
application.sourceType === "docker"
? application.dockerImage
: application.appName;
await createRollback({
appName: tagImage || "",
deploymentId: deployment.deploymentId,
});
}
await sendBuildSuccessNotifications({
projectName: application.environment.project.name,
applicationName: application.name,
applicationType: "application",
buildLink,
organizationId: application.environment.project.organizationId,
domains: application.domains,
});
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
const encodedContent = encodeBase64(errorMessage);
await execAsyncRemote(
application.serverId,
`
echo "\n\n===================================EXTRA LOGS============================================" >> ${deployment.logPath};
echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath};
echo "${encodedContent}" | base64 -d >> "${deployment.logPath}";`,
);
await updateDeploymentStatus(deployment.deploymentId, "error"); await updateDeploymentStatus(deployment.deploymentId, "error");
await updateApplicationStatus(applicationId, "error"); await updateApplicationStatus(applicationId, "error");
await sendBuildErrorNotifications({
projectName: application.environment.project.name,
applicationName: application.name,
applicationType: "application",
errorMessage: `Please check the logs for details: ${errorMessage}`,
buildLink,
organizationId: application.environment.project.organizationId,
});
throw error; throw error;
} }
@@ -475,14 +372,22 @@ export const deployPreviewApplication = async ({
application.buildArgs = `${application.previewBuildArgs}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`; application.buildArgs = `${application.previewBuildArgs}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
application.buildSecrets = `${application.previewBuildSecrets}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`; application.buildSecrets = `${application.previewBuildSecrets}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
let command = "set -e;";
if (application.sourceType === "github") { if (application.sourceType === "github") {
await cloneGithubRepository({ command += await cloneGithubRepository({
...application, ...application,
appName: previewDeployment.appName, appName: previewDeployment.appName,
branch: previewDeployment.branch, branch: previewDeployment.branch,
logPath: deployment.logPath,
}); });
await buildApplication(application, deployment.logPath); command += getBuildCommand(application);
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
if (application.serverId) {
await execAsyncRemote(application.serverId, commandWithLog);
} else {
await execAsync(commandWithLog);
}
await mechanizeDockerContainer(application);
} }
const successComment = getIssueComment( const successComment = getIssueComment(
application.name, application.name,
@@ -513,170 +418,10 @@ export const deployPreviewApplication = async ({
return true; return true;
}; };
export const deployRemotePreviewApplication = async ({
applicationId,
titleLog = "Preview Deployment",
descriptionLog = "",
previewDeploymentId,
}: {
applicationId: string;
titleLog: string;
descriptionLog: string;
previewDeploymentId: string;
}) => {
const application = await findApplicationById(applicationId);
const deployment = await createDeploymentPreview({
title: titleLog,
description: descriptionLog,
previewDeploymentId: previewDeploymentId,
});
const previewDeployment =
await findPreviewDeploymentById(previewDeploymentId);
await updatePreviewDeployment(previewDeploymentId, {
createdAt: new Date().toISOString(),
});
const previewDomain = getDomainHost(previewDeployment?.domain as Domain);
const issueParams = {
owner: application?.owner || "",
repository: application?.repository || "",
issue_number: previewDeployment.pullRequestNumber,
comment_id: Number.parseInt(previewDeployment.pullRequestCommentId),
githubId: application?.githubId || "",
};
try {
const commentExists = await issueCommentExists({
...issueParams,
});
if (!commentExists) {
const result = await createPreviewDeploymentComment({
...issueParams,
previewDomain,
appName: previewDeployment.appName,
githubId: application?.githubId || "",
previewDeploymentId,
});
if (!result) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Pull request comment not found",
});
}
issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId);
}
const buildingComment = getIssueComment(
application.name,
"running",
previewDomain,
);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
});
application.appName = previewDeployment.appName;
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
application.buildArgs = `${application.previewBuildArgs}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
application.buildSecrets = `${application.previewBuildSecrets}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
if (application.serverId) {
let command = "set -e;";
if (application.sourceType === "github") {
command += await getGithubCloneCommand({
...application,
appName: previewDeployment.appName,
branch: previewDeployment.branch,
serverId: application.serverId,
logPath: deployment.logPath,
});
}
command += getBuildCommand(application, deployment.logPath);
await execAsyncRemote(application.serverId, command);
await mechanizeDockerContainer(application);
}
const successComment = getIssueComment(
application.name,
"success",
previewDomain,
);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${successComment}`,
});
await updateDeploymentStatus(deployment.deploymentId, "done");
await updatePreviewDeployment(previewDeploymentId, {
previewStatus: "done",
});
} catch (error) {
const comment = getIssueComment(application.name, "error", previewDomain);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${comment}`,
});
await updateDeploymentStatus(deployment.deploymentId, "error");
await updatePreviewDeployment(previewDeploymentId, {
previewStatus: "error",
});
throw error;
}
return true;
};
export const rebuildRemoteApplication = async ({
applicationId,
titleLog = "Rebuild deployment",
descriptionLog = "",
}: {
applicationId: string;
titleLog: string;
descriptionLog: string;
}) => {
const application = await findApplicationById(applicationId);
const deployment = await createDeployment({
applicationId: applicationId,
title: titleLog,
description: descriptionLog,
});
try {
if (application.serverId) {
if (application.sourceType !== "docker") {
let command = "set -e;";
command += getBuildCommand(application, deployment.logPath);
await execAsyncRemote(application.serverId, command);
}
await mechanizeDockerContainer(application);
}
await updateDeploymentStatus(deployment.deploymentId, "done");
await updateApplicationStatus(applicationId, "done");
} catch (error) {
// @ts-ignore
const encodedContent = encodeBase64(error?.message);
await execAsyncRemote(
application.serverId,
`
echo "\n\n===================================EXTRA LOGS============================================" >> ${deployment.logPath};
echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath};
echo "${encodedContent}" | base64 -d >> "${deployment.logPath}";`,
);
await updateDeploymentStatus(deployment.deploymentId, "error");
await updateApplicationStatus(applicationId, "error");
throw error;
}
return true;
};
export const getApplicationStats = async (appName: string) => { export const getApplicationStats = async (appName: string) => {
if (appName === "dokploy") {
return await getAdvancedStats(appName);
}
const filter = { const filter = {
status: ["running"], status: ["running"],
label: [`com.docker.swarm.service.name=${appName}`], label: [`com.docker.swarm.service.name=${appName}`],
+1
View File
@@ -616,6 +616,7 @@ const ARVANCLOUD_IP_RANGES = [
"37.32.18.0/27", "37.32.18.0/27",
"37.32.19.0/27", "37.32.19.0/27",
"185.215.232.0/22", "185.215.232.0/22",
"178.131.120.48/28",
]; ];
const CDN_PROVIDERS: CDNProvider[] = [ const CDN_PROVIDERS: CDNProvider[] = [
+67 -193
View File
@@ -7,14 +7,10 @@ import {
cleanAppName, cleanAppName,
compose, compose,
} from "@dokploy/server/db/schema"; } from "@dokploy/server/db/schema";
import { import { getBuildComposeCommand } from "@dokploy/server/utils/builders/compose";
buildCompose,
getBuildComposeCommand,
} from "@dokploy/server/utils/builders/compose";
import { randomizeSpecificationFile } from "@dokploy/server/utils/docker/compose"; import { randomizeSpecificationFile } from "@dokploy/server/utils/docker/compose";
import { import {
cloneCompose, cloneCompose,
cloneComposeRemote,
loadDockerCompose, loadDockerCompose,
loadDockerComposeRemote, loadDockerComposeRemote,
} from "@dokploy/server/utils/docker/domain"; } from "@dokploy/server/utils/docker/domain";
@@ -25,35 +21,23 @@ import {
execAsync, execAsync,
execAsyncRemote, execAsyncRemote,
} from "@dokploy/server/utils/process/execAsync"; } from "@dokploy/server/utils/process/execAsync";
import { import { cloneBitbucketRepository } from "@dokploy/server/utils/providers/bitbucket";
cloneBitbucketRepository,
getBitbucketCloneCommand,
} from "@dokploy/server/utils/providers/bitbucket";
import { import {
cloneGitRepository, cloneGitRepository,
getCustomGitCloneCommand, getGitCommitInfo,
} from "@dokploy/server/utils/providers/git"; } from "@dokploy/server/utils/providers/git";
import { import { cloneGiteaRepository } from "@dokploy/server/utils/providers/gitea";
cloneGiteaRepository, import { cloneGithubRepository } from "@dokploy/server/utils/providers/github";
getGiteaCloneCommand, import { cloneGitlabRepository } from "@dokploy/server/utils/providers/gitlab";
} from "@dokploy/server/utils/providers/gitea"; import { getCreateComposeFileCommand } from "@dokploy/server/utils/providers/raw";
import {
cloneGithubRepository,
getGithubCloneCommand,
} from "@dokploy/server/utils/providers/github";
import {
cloneGitlabRepository,
getGitlabCloneCommand,
} from "@dokploy/server/utils/providers/gitlab";
import {
createComposeFile,
getCreateComposeFileCommand,
} from "@dokploy/server/utils/providers/raw";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { encodeBase64 } from "../utils/docker/utils";
import { getDokployUrl } from "./admin"; import { getDokployUrl } from "./admin";
import { createDeploymentCompose, updateDeploymentStatus } from "./deployment"; import {
createDeploymentCompose,
updateDeployment,
updateDeploymentStatus,
} from "./deployment";
import { validUniqueServerAppName } from "./project"; import { validUniqueServerAppName } from "./project";
export type Compose = typeof compose.$inferSelect; export type Compose = typeof compose.$inferSelect;
@@ -163,10 +147,11 @@ export const loadServices = async (
const compose = await findComposeById(composeId); const compose = await findComposeById(composeId);
if (type === "fetch") { if (type === "fetch") {
const command = await cloneCompose(compose);
if (compose.serverId) { if (compose.serverId) {
await cloneComposeRemote(compose); await execAsyncRemote(compose.serverId, command);
} else { } else {
await cloneCompose(compose); await execAsync(command);
} }
} }
@@ -235,24 +220,41 @@ export const deployCompose = async ({
}); });
try { try {
const entity = {
...compose,
type: "compose" as const,
};
let command = "set -e;";
if (compose.sourceType === "github") { if (compose.sourceType === "github") {
await cloneGithubRepository({ command += await cloneGithubRepository(entity);
...compose,
logPath: deployment.logPath,
type: "compose",
});
} else if (compose.sourceType === "gitlab") { } else if (compose.sourceType === "gitlab") {
await cloneGitlabRepository(compose, deployment.logPath, true); command += await cloneGitlabRepository(entity);
} else if (compose.sourceType === "bitbucket") { } else if (compose.sourceType === "bitbucket") {
await cloneBitbucketRepository(compose, deployment.logPath, true); command += await cloneBitbucketRepository(entity);
} else if (compose.sourceType === "git") { } else if (compose.sourceType === "git") {
await cloneGitRepository(compose, deployment.logPath, true); command += await cloneGitRepository(entity);
} else if (compose.sourceType === "gitea") { } else if (compose.sourceType === "gitea") {
await cloneGiteaRepository(compose, deployment.logPath, true); command += await cloneGiteaRepository(entity);
} else if (compose.sourceType === "raw") { } else if (compose.sourceType === "raw") {
await createComposeFile(compose, deployment.logPath); command += getCreateComposeFileCommand(entity);
} }
await buildCompose(compose, deployment.logPath);
let commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
if (compose.serverId) {
await execAsyncRemote(compose.serverId, commandWithLog);
} else {
await execAsync(commandWithLog);
}
command = "set -e;";
command += await getBuildComposeCommand(entity);
commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
if (compose.serverId) {
await execAsyncRemote(compose.serverId, commandWithLog);
} else {
await execAsync(commandWithLog);
}
await updateDeploymentStatus(deployment.deploymentId, "done"); await updateDeploymentStatus(deployment.deploymentId, "done");
await updateCompose(composeId, { await updateCompose(composeId, {
composeStatus: "done", composeStatus: "done",
@@ -281,6 +283,19 @@ export const deployCompose = async ({
organizationId: compose.environment.project.organizationId, organizationId: compose.environment.project.organizationId,
}); });
throw error; throw error;
} finally {
if (compose.sourceType !== "raw") {
const commitInfo = await getGitCommitInfo({
...compose,
type: "compose",
});
if (commitInfo) {
await updateDeployment(deployment.deploymentId, {
title: commitInfo.message,
description: `Commit: ${commitInfo.hash}`,
});
}
}
} }
}; };
@@ -302,154 +317,23 @@ export const rebuildCompose = async ({
}); });
try { try {
let command = "set -e;";
if (compose.sourceType === "raw") { if (compose.sourceType === "raw") {
await createComposeFile(compose, deployment.logPath); command += getCreateComposeFileCommand(compose);
} }
await buildCompose(compose, deployment.logPath);
await updateDeploymentStatus(deployment.deploymentId, "done"); let commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
await updateCompose(composeId, {
composeStatus: "done",
});
} catch (error) {
await updateDeploymentStatus(deployment.deploymentId, "error");
await updateCompose(composeId, {
composeStatus: "error",
});
throw error;
}
return true;
};
export const deployRemoteCompose = async ({
composeId,
titleLog = "Manual deployment",
descriptionLog = "",
}: {
composeId: string;
titleLog: string;
descriptionLog: string;
}) => {
const compose = await findComposeById(composeId);
const buildLink = `${await getDokployUrl()}/dashboard/project/${
compose.environment.projectId
}/environment/${compose.environmentId}/services/compose/${compose.composeId}?tab=deployments`;
const deployment = await createDeploymentCompose({
composeId: composeId,
title: titleLog,
description: descriptionLog,
});
try {
if (compose.serverId) { if (compose.serverId) {
let command = "set -e;"; await execAsyncRemote(compose.serverId, commandWithLog);
} else {
if (compose.sourceType === "github") { await execAsync(commandWithLog);
command += await getGithubCloneCommand({
...compose,
logPath: deployment.logPath,
type: "compose",
serverId: compose.serverId,
});
} else if (compose.sourceType === "gitlab") {
command += await getGitlabCloneCommand(
compose,
deployment.logPath,
true,
);
} else if (compose.sourceType === "bitbucket") {
command += await getBitbucketCloneCommand(
compose,
deployment.logPath,
true,
);
} else if (compose.sourceType === "git") {
command += await getCustomGitCloneCommand(
compose,
deployment.logPath,
true,
);
console.log(command);
} else if (compose.sourceType === "raw") {
command += getCreateComposeFileCommand(compose, deployment.logPath);
} else if (compose.sourceType === "gitea") {
command += await getGiteaCloneCommand(
compose,
deployment.logPath,
true,
);
}
await execAsyncRemote(compose.serverId, command);
await getBuildComposeCommand(compose, deployment.logPath);
}
await updateDeploymentStatus(deployment.deploymentId, "done");
await updateCompose(composeId, {
composeStatus: "done",
});
await sendBuildSuccessNotifications({
projectName: compose.environment.project.name,
applicationName: compose.name,
applicationType: "compose",
buildLink,
organizationId: compose.environment.project.organizationId,
domains: compose.domains,
});
} catch (error) {
// @ts-ignore
const encodedContent = encodeBase64(error?.message);
await execAsyncRemote(
compose.serverId,
`
echo "\n\n===================================EXTRA LOGS============================================" >> ${deployment.logPath};
echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath};
echo "${encodedContent}" | base64 -d >> "${deployment.logPath}";`,
);
await updateDeploymentStatus(deployment.deploymentId, "error");
await updateCompose(composeId, {
composeStatus: "error",
});
await sendBuildErrorNotifications({
projectName: compose.environment.project.name,
applicationName: compose.name,
applicationType: "compose",
// @ts-ignore
errorMessage: error?.message || "Error building",
buildLink,
organizationId: compose.environment.project.organizationId,
});
throw error;
}
};
export const rebuildRemoteCompose = async ({
composeId,
titleLog = "Rebuild deployment",
descriptionLog = "",
}: {
composeId: string;
titleLog: string;
descriptionLog: string;
}) => {
const compose = await findComposeById(composeId);
const deployment = await createDeploymentCompose({
composeId: composeId,
title: titleLog,
description: descriptionLog,
});
try {
if (compose.sourceType === "raw") {
const command = getCreateComposeFileCommand(compose, deployment.logPath);
await execAsyncRemote(compose.serverId, command);
} }
command += await getBuildComposeCommand(compose);
commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
if (compose.serverId) { if (compose.serverId) {
await getBuildComposeCommand(compose, deployment.logPath); await execAsyncRemote(compose.serverId, commandWithLog);
} else {
await execAsync(commandWithLog);
} }
await updateDeploymentStatus(deployment.deploymentId, "done"); await updateDeploymentStatus(deployment.deploymentId, "done");
@@ -457,16 +341,6 @@ export const rebuildRemoteCompose = async ({
composeStatus: "done", composeStatus: "done",
}); });
} catch (error) { } catch (error) {
// @ts-ignore
const encodedContent = encodeBase64(error?.message);
await execAsyncRemote(
compose.serverId,
`
echo "\n\n===================================EXTRA LOGS============================================" >> ${deployment.logPath};
echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath};
echo "${encodedContent}" | base64 -d >> "${deployment.logPath}";`,
);
await updateDeploymentStatus(deployment.deploymentId, "error"); await updateDeploymentStatus(deployment.deploymentId, "error");
await updateCompose(composeId, { await updateCompose(composeId, {
composeStatus: "error", composeStatus: "error",
+8 -7
View File
@@ -74,20 +74,21 @@ export const createDeployment = async (
>, >,
) => { ) => {
const application = await findApplicationById(deployment.applicationId); const application = await findApplicationById(deployment.applicationId);
try { try {
await removeLastTenDeployments( await removeLastTenDeployments(
deployment.applicationId, deployment.applicationId,
"application", "application",
application.serverId, application.serverId,
); );
const { LOGS_PATH } = paths(!!application.serverId); const serverId = application.serverId;
const { LOGS_PATH } = paths(!!serverId);
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss"); const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
const fileName = `${application.appName}-${formattedDateTime}.log`; const fileName = `${application.appName}-${formattedDateTime}.log`;
const logFilePath = path.join(LOGS_PATH, application.appName, fileName); const logFilePath = path.join(LOGS_PATH, application.appName, fileName);
if (application.serverId) { if (serverId) {
const server = await findServerById(application.serverId); const server = await findServerById(serverId);
const command = ` const command = `
mkdir -p ${LOGS_PATH}/${application.appName}; mkdir -p ${LOGS_PATH}/${application.appName};
@@ -99,7 +100,7 @@ export const createDeployment = async (
await fsPromises.mkdir(path.join(LOGS_PATH, application.appName), { await fsPromises.mkdir(path.join(LOGS_PATH, application.appName), {
recursive: true, recursive: true,
}); });
await fsPromises.writeFile(logFilePath, "Initializing deployment"); await fsPromises.writeFile(logFilePath, "Initializing deployment\n");
} }
const deploymentCreate = await db const deploymentCreate = await db
@@ -249,7 +250,7 @@ export const createDeploymentCompose = async (
const command = ` const command = `
mkdir -p ${LOGS_PATH}/${compose.appName}; mkdir -p ${LOGS_PATH}/${compose.appName};
echo "Initializing deployment" >> ${logFilePath}; echo "Initializing deployment\n" >> ${logFilePath};
`; `;
await execAsyncRemote(server.serverId, command); await execAsyncRemote(server.serverId, command);
@@ -257,7 +258,7 @@ echo "Initializing deployment" >> ${logFilePath};
await fsPromises.mkdir(path.join(LOGS_PATH, compose.appName), { await fsPromises.mkdir(path.join(LOGS_PATH, compose.appName), {
recursive: true, recursive: true,
}); });
await fsPromises.writeFile(logFilePath, "Initializing deployment"); await fsPromises.writeFile(logFilePath, "Initializing deployment\n");
} }
const deploymentCreate = await db const deploymentCreate = await db
+37 -7
View File
@@ -34,13 +34,43 @@ export const findEnvironmentById = async (environmentId: string) => {
const environment = await db.query.environments.findFirst({ const environment = await db.query.environments.findFirst({
where: eq(environments.environmentId, environmentId), where: eq(environments.environmentId, environmentId),
with: { with: {
applications: true, applications: {
mariadb: true, with: {
mongo: true, deployments: true,
mysql: true, server: true,
postgres: true, },
redis: true, },
compose: true, mariadb: {
with: {
server: true,
},
},
mongo: {
with: {
server: true,
},
},
mysql: {
with: {
server: true,
},
},
postgres: {
with: {
server: true,
},
},
redis: {
with: {
server: true,
},
},
compose: {
with: {
deployments: true,
server: true,
},
},
project: true, project: true,
}, },
}); });
+2 -1
View File
@@ -59,7 +59,8 @@ export const getUpdateData = async (): Promise<IUpdateData> => {
let currentDigest: string; let currentDigest: string;
try { try {
currentDigest = await getServiceImageDigest(); currentDigest = await getServiceImageDigest();
} catch { } catch (error) {
console.error(error);
// Docker service might not exist locally // Docker service might not exist locally
// You can run the # Installation command for docker service create mentioned in the below docs to test it locally: // You can run the # Installation command for docker service create mentioned in the below docs to test it locally:
// https://docs.dokploy.com/docs/core/manual-installation // https://docs.dokploy.com/docs/core/manual-installation
+6 -6
View File
@@ -1,10 +1,10 @@
import { db } from "@dokploy/server/db"; import { db } from "@dokploy/server/db";
import { apikey, member, users_temp } from "@dokploy/server/db/schema"; import { apikey, member, user } from "@dokploy/server/db/schema";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { auth } from "../lib/auth"; import { auth } from "../lib/auth";
export type User = typeof users_temp.$inferSelect; export type User = typeof user.$inferSelect;
export const addNewProject = async ( export const addNewProject = async (
userId: string, userId: string,
@@ -403,16 +403,16 @@ export const updateUser = async (userId: string, userData: Partial<User>) => {
} }
} }
const user = await db const userResult = await db
.update(users_temp) .update(user)
.set({ .set({
...userData, ...userData,
}) })
.where(eq(users_temp.id, userId)) .where(eq(user.id, userId))
.returning() .returning()
.then((res) => res[0]); .then((res) => res[0]);
return user; return userResult;
}; };
export const createApiKey = async ( export const createApiKey = async (
+1 -1
View File
@@ -20,7 +20,7 @@ export const TRAEFIK_PORT =
Number.parseInt(process.env.TRAEFIK_PORT!, 10) || 80; Number.parseInt(process.env.TRAEFIK_PORT!, 10) || 80;
export const TRAEFIK_HTTP3_PORT = export const TRAEFIK_HTTP3_PORT =
Number.parseInt(process.env.TRAEFIK_HTTP3_PORT!, 10) || 443; Number.parseInt(process.env.TRAEFIK_HTTP3_PORT!, 10) || 443;
export const TRAEFIK_VERSION = process.env.TRAEFIK_VERSION || "3.5.0"; export const TRAEFIK_VERSION = process.env.TRAEFIK_VERSION || "3.6.1";
export interface TraefikOptions { export interface TraefikOptions {
env?: string[]; env?: string[];
+10 -1
View File
@@ -37,7 +37,16 @@ export const generateRandomDomain = ({
const hash = randomBytes(3).toString("hex"); const hash = randomBytes(3).toString("hex");
const slugIp = serverIp.replaceAll(".", "-").replaceAll(":", "-"); const slugIp = serverIp.replaceAll(".", "-").replaceAll(":", "-");
return `${projectName}-${hash}${slugIp === "" ? "" : `-${slugIp}`}.traefik.me`; // Domain labels have a max length of 63 characters
// Reserve space for: hash (6) + separators (1-2) + ip section + dot + traefik.me (10)
// Approx: 6 + 2 + (variable ip length) + 11 = ~19-30 chars for other parts
const maxProjectNameLength = 40;
const truncatedProjectName =
projectName.length > maxProjectNameLength
? projectName.substring(0, maxProjectNameLength)
: projectName;
return `${truncatedProjectName}-${hash}${slugIp === "" ? "" : `-${slugIp}`}.traefik.me`;
}; };
export const generateHash = (length = 8): string => { export const generateHash = (length = 8): string => {
+11 -129
View File
@@ -1,117 +1,28 @@
import {
createWriteStream,
existsSync,
mkdirSync,
writeFileSync,
} from "node:fs";
import { dirname, join } from "node:path"; import { dirname, join } from "node:path";
import { paths } from "@dokploy/server/constants"; import { paths } from "@dokploy/server/constants";
import type { InferResultType } from "@dokploy/server/types/with"; import type { InferResultType } from "@dokploy/server/types/with";
import boxen from "boxen"; import boxen from "boxen";
import { import { writeDomainsToComposeRemote } from "../docker/domain";
writeDomainsToCompose,
writeDomainsToComposeRemote,
} from "../docker/domain";
import { import {
encodeBase64, encodeBase64,
getEnviromentVariablesObject, getEnviromentVariablesObject,
prepareEnvironmentVariables, prepareEnvironmentVariables,
} from "../docker/utils"; } from "../docker/utils";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { spawnAsync } from "../process/spawnAsync";
export type ComposeNested = InferResultType< export type ComposeNested = InferResultType<
"compose", "compose",
{ environment: { with: { project: true } }; mounts: true; domains: true } { environment: { with: { project: true } }; mounts: true; domains: true }
>; >;
export const buildCompose = async (compose: ComposeNested, logPath: string) => {
const writeStream = createWriteStream(logPath, { flags: "a" });
const { sourceType, appName, mounts, composeType, domains } = compose;
try {
const { COMPOSE_PATH } = paths();
const command = createCommand(compose);
await writeDomainsToCompose(compose, domains);
createEnvFile(compose);
if (compose.isolatedDeployment) { export const getBuildComposeCommand = async (compose: ComposeNested) => {
await execAsync( const { COMPOSE_PATH } = paths(!!compose.serverId);
`docker network inspect ${compose.appName} >/dev/null 2>&1 || docker network create ${composeType === "stack" ? "--driver overlay" : ""} --attachable ${compose.appName}`,
);
}
const logContent = `
App Name: ${appName}
Build Compose 🐳
Detected: ${mounts.length} mounts 📂
Command: docker ${command}
Source Type: docker ${sourceType} ✅
Compose Type: ${composeType} ✅`;
const logBox = boxen(logContent, {
padding: {
left: 1,
right: 1,
bottom: 1,
},
width: 80,
borderStyle: "double",
});
writeStream.write(`\n${logBox}\n`);
const projectPath = join(COMPOSE_PATH, compose.appName, "code");
await spawnAsync(
"docker",
[...command.split(" ")],
(data) => {
if (writeStream.writable) {
writeStream.write(data.toString());
}
},
{
cwd: projectPath,
env: {
NODE_ENV: process.env.NODE_ENV,
PATH: process.env.PATH,
...(composeType === "stack" && {
...getEnviromentVariablesObject(
compose.env,
compose.environment.project.env,
),
}),
},
},
);
if (compose.isolatedDeployment) {
await execAsync(
`docker network connect ${compose.appName} $(docker ps --filter "name=dokploy-traefik" -q) >/dev/null 2>&1`,
).catch(() => {});
}
writeStream.write("Docker Compose Deployed: ✅");
} catch (error) {
writeStream.write(`Error ❌ ${(error as Error).message}`);
throw error;
} finally {
writeStream.end();
}
};
export const getBuildComposeCommand = async (
compose: ComposeNested,
logPath: string,
) => {
const { COMPOSE_PATH } = paths(true);
const { sourceType, appName, mounts, composeType, domains } = compose; const { sourceType, appName, mounts, composeType, domains } = compose;
const command = createCommand(compose); const command = createCommand(compose);
const envCommand = getCreateEnvFileCommand(compose); const envCommand = getCreateEnvFileCommand(compose);
const projectPath = join(COMPOSE_PATH, compose.appName, "code"); const projectPath = join(COMPOSE_PATH, compose.appName, "code");
const exportEnvCommand = getExportEnvCommand(compose); const exportEnvCommand = getExportEnvCommand(compose);
const newCompose = await writeDomainsToComposeRemote( const newCompose = await writeDomainsToComposeRemote(compose, domains);
compose,
domains,
logPath,
);
const logContent = ` const logContent = `
App Name: ${appName} App Name: ${appName}
Build Compose 🐳 Build Compose 🐳
@@ -133,7 +44,7 @@ Compose Type: ${composeType} ✅`;
const bashCommand = ` const bashCommand = `
set -e set -e
{ {
echo "${logBox}" >> "${logPath}" echo "${logBox}";
${newCompose} ${newCompose}
@@ -143,17 +54,18 @@ Compose Type: ${composeType} ✅`;
${exportEnvCommand} ${exportEnvCommand}
${compose.isolatedDeployment ? `docker network inspect ${compose.appName} >/dev/null 2>&1 || docker network create --attachable ${compose.appName}` : ""} ${compose.isolatedDeployment ? `docker network inspect ${compose.appName} >/dev/null 2>&1 || docker network create --attachable ${compose.appName}` : ""}
docker ${command.split(" ").join(" ")} >> "${logPath}" 2>&1 || { echo "Error: ❌ Docker command failed" >> "${logPath}"; exit 1; } docker ${command.split(" ").join(" ")} 2>&1 || { echo "Error: ❌ Docker command failed"; exit 1; }
${compose.isolatedDeployment ? `docker network connect ${compose.appName} $(docker ps --filter "name=dokploy-traefik" -q) >/dev/null 2>&1` : ""} ${compose.isolatedDeployment ? `docker network connect ${compose.appName} $(docker ps --filter "name=dokploy-traefik" -q) >/dev/null 2>&1` : ""}
echo "Docker Compose Deployed: ✅" >> "${logPath}" echo "Docker Compose Deployed: ✅";
} || { } || {
echo "Error: ❌ Script execution failed" >> "${logPath}" echo "Error: ❌ Script execution failed";
exit 1 exit 1
} }
`; `;
return await execAsyncRemote(compose.serverId, bashCommand); return bashCommand;
// return await execAsyncRemote(compose.serverId, bashCommand);
}; };
const sanitizeCommand = (command: string) => { const sanitizeCommand = (command: string) => {
@@ -185,38 +97,8 @@ export const createCommand = (compose: ComposeNested) => {
return command; return command;
}; };
const createEnvFile = (compose: ComposeNested) => {
const { COMPOSE_PATH } = paths();
const { env, composePath, appName } = compose;
const composeFilePath =
join(COMPOSE_PATH, appName, "code", composePath) ||
join(COMPOSE_PATH, appName, "code", "docker-compose.yml");
const envFilePath = join(dirname(composeFilePath), ".env");
let envContent = `APP_NAME=${appName}\n`;
envContent += env || "";
if (!envContent.includes("DOCKER_CONFIG")) {
envContent += "\nDOCKER_CONFIG=/root/.docker";
}
if (compose.randomize) {
envContent += `\nCOMPOSE_PREFIX=${compose.suffix}`;
}
const envFileContent = prepareEnvironmentVariables(
envContent,
compose.environment.project.env,
compose.environment.env,
).join("\n");
if (!existsSync(dirname(envFilePath))) {
mkdirSync(dirname(envFilePath), { recursive: true });
}
writeFileSync(envFilePath, envFileContent);
};
export const getCreateEnvFileCommand = (compose: ComposeNested) => { export const getCreateEnvFileCommand = (compose: ComposeNested) => {
const { COMPOSE_PATH } = paths(true); const { COMPOSE_PATH } = paths(!!compose.serverId);
const { env, composePath, appName } = compose; const { env, composePath, appName } = compose;
const composeFilePath = const composeFilePath =
join(COMPOSE_PATH, appName, "code", composePath) || join(COMPOSE_PATH, appName, "code", composePath) ||
@@ -1,4 +1,3 @@
import type { WriteStream } from "node:fs";
import { import {
getEnviromentVariablesObject, getEnviromentVariablesObject,
prepareEnvironmentVariables, prepareEnvironmentVariables,
@@ -7,103 +6,10 @@ import {
getBuildAppDirectory, getBuildAppDirectory,
getDockerContextPath, getDockerContextPath,
} from "../filesystem/directory"; } from "../filesystem/directory";
import { spawnAsync } from "../process/spawnAsync";
import type { ApplicationNested } from "."; import type { ApplicationNested } from ".";
import { createEnvFile, createEnvFileCommand } from "./utils"; import { createEnvFileCommand } from "./utils";
export const buildCustomDocker = async ( export const getDockerCommand = (application: ApplicationNested) => {
application: ApplicationNested,
writeStream: WriteStream,
) => {
const {
appName,
env,
publishDirectory,
buildArgs,
buildSecrets,
dockerBuildStage,
cleanCache,
} = application;
const dockerFilePath = getBuildAppDirectory(application);
try {
const image = `${appName}`;
const defaultContextPath =
dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || ".";
const dockerContextPath = getDockerContextPath(application);
const commandArgs = ["build", "-t", image, "-f", dockerFilePath, "."];
if (cleanCache) {
commandArgs.push("--no-cache");
}
if (dockerBuildStage) {
commandArgs.push("--target", dockerBuildStage);
}
const args = prepareEnvironmentVariables(
buildArgs,
application.environment.project.env,
application.environment.env,
);
for (const arg of args) {
commandArgs.push("--build-arg", arg);
}
const secrets = getEnviromentVariablesObject(
buildSecrets,
application.environment.project.env,
application.environment.env,
);
for (const key in secrets) {
// Although buildx is smart enough to know we may be referring to an environment variable name,
// we still make sure it doesn't fall back to type=file.
// See: https://docs.docker.com/reference/cli/docker/buildx/build/#secret
commandArgs.push("--secret", `type=env,id=${key}`);
}
/*
Do not generate an environment file when publishDirectory is specified,
as it could be publicly exposed.
*/
if (!publishDirectory) {
createEnvFile(
dockerFilePath,
env,
application.environment.project.env,
application.environment.env,
);
}
await spawnAsync(
"docker",
commandArgs,
(data) => {
if (writeStream.writable) {
writeStream.write(data);
}
},
{
cwd: dockerContextPath || defaultContextPath,
env: {
...process.env,
...secrets,
},
},
);
} catch (error) {
throw error;
}
};
export const getDockerCommand = (
application: ApplicationNested,
logPath: string,
) => {
const { const {
appName, appName,
env, env,
@@ -176,17 +82,17 @@ export const getDockerCommand = (
} }
command += ` command += `
echo "Building ${appName}" >> ${logPath}; echo "Building ${appName}" ;
cd ${dockerContextPath} >> ${logPath} 2>> ${logPath} || { cd ${dockerContextPath} || {
echo "❌ The path ${dockerContextPath} does not exist" >> ${logPath}; echo "❌ The path ${dockerContextPath} does not exist" ;
exit 1; exit 1;
} }
${joinedSecrets} docker ${commandArgs.join(" ")} >> ${logPath} 2>> ${logPath} || { ${joinedSecrets} docker ${commandArgs.join(" ")} || {
echo "❌ Docker build failed" >> ${logPath}; echo "❌ Docker build failed" ;
exit 1; exit 1;
} }
echo "✅ Docker build completed." >> ${logPath}; echo "✅ Docker build completed." ;
`; `;
return command; return command;
+5 -51
View File
@@ -1,54 +1,8 @@
import type { WriteStream } from "node:fs";
import { prepareEnvironmentVariables } from "../docker/utils"; import { prepareEnvironmentVariables } from "../docker/utils";
import { getBuildAppDirectory } from "../filesystem/directory"; import { getBuildAppDirectory } from "../filesystem/directory";
import { spawnAsync } from "../process/spawnAsync";
import type { ApplicationNested } from "."; import type { ApplicationNested } from ".";
// TODO: integrate in the vps sudo chown -R $(whoami) ~/.docker export const getHerokuCommand = (application: ApplicationNested) => {
export const buildHeroku = async (
application: ApplicationNested,
writeStream: WriteStream,
) => {
const { env, appName, cleanCache } = application;
const buildAppDirectory = getBuildAppDirectory(application);
const envVariables = prepareEnvironmentVariables(
env,
application.environment.project.env,
application.environment.env,
);
try {
const args = [
"build",
appName,
"--path",
buildAppDirectory,
"--builder",
`heroku/builder:${application.herokuVersion || "24"}`,
];
for (const env of envVariables) {
args.push("--env", env);
}
if (cleanCache) {
args.push("--clear-cache");
}
await spawnAsync("pack", args, (data) => {
if (writeStream.writable) {
writeStream.write(data);
}
});
return true;
} catch (e) {
throw e;
}
};
export const getHerokuCommand = (
application: ApplicationNested,
logPath: string,
) => {
const { env, appName, cleanCache } = application; const { env, appName, cleanCache } = application;
const buildAppDirectory = getBuildAppDirectory(application); const buildAppDirectory = getBuildAppDirectory(application);
@@ -77,12 +31,12 @@ export const getHerokuCommand = (
const command = `pack ${args.join(" ")}`; const command = `pack ${args.join(" ")}`;
const bashCommand = ` const bashCommand = `
echo "Starting heroku build..." >> ${logPath}; echo "Starting heroku build..." ;
${command} >> ${logPath} 2>> ${logPath} || { ${command} || {
echo "❌ Heroku build failed" >> ${logPath}; echo "❌ Heroku build failed" ;
exit 1; exit 1;
} }
echo "✅ Heroku build completed." >> ${logPath}; echo "✅ Heroku build completed." ;
`; `;
return bashCommand; return bashCommand;
+30 -69
View File
@@ -1,7 +1,6 @@
import { createWriteStream } from "node:fs";
import type { InferResultType } from "@dokploy/server/types/with"; import type { InferResultType } from "@dokploy/server/types/with";
import type { CreateServiceOptions } from "dockerode"; import type { CreateServiceOptions } from "dockerode";
import { uploadImage, uploadImageRemoteCommand } from "../cluster/upload"; import { uploadImageRemoteCommand } from "../cluster/upload";
import { import {
calculateResources, calculateResources,
generateBindMounts, generateBindMounts,
@@ -11,12 +10,12 @@ import {
prepareEnvironmentVariables, prepareEnvironmentVariables,
} from "../docker/utils"; } from "../docker/utils";
import { getRemoteDocker } from "../servers/remote-docker"; import { getRemoteDocker } from "../servers/remote-docker";
import { buildCustomDocker, getDockerCommand } from "./docker-file"; import { getDockerCommand } from "./docker-file";
import { buildHeroku, getHerokuCommand } from "./heroku"; import { getHerokuCommand } from "./heroku";
import { buildNixpacks, getNixpacksCommand } from "./nixpacks"; import { getNixpacksCommand } from "./nixpacks";
import { buildPaketo, getPaketoCommand } from "./paketo"; import { getPaketoCommand } from "./paketo";
import { buildRailpack, getRailpackCommand } from "./railpack"; import { getRailpackCommand } from "./railpack";
import { buildStatic, getStaticCommand } from "./static"; import { getStaticCommand } from "./static";
// NIXPACKS codeDirectory = where is the path of the code directory // NIXPACKS codeDirectory = where is the path of the code directory
// HEROKU codeDirectory = where is the path of the code directory // HEROKU codeDirectory = where is the path of the code directory
@@ -34,76 +33,35 @@ export type ApplicationNested = InferResultType<
} }
>; >;
export const buildApplication = async ( export const getBuildCommand = (application: ApplicationNested) => {
application: ApplicationNested,
logPath: string,
) => {
const writeStream = createWriteStream(logPath, { flags: "a" });
const { buildType, sourceType } = application;
try {
writeStream.write(
`\nBuild ${buildType}: ✅\nSource Type: ${sourceType}: ✅\n`,
);
console.log(`Build ${buildType}: ✅`);
if (buildType === "nixpacks") {
await buildNixpacks(application, writeStream);
} else if (buildType === "heroku_buildpacks") {
await buildHeroku(application, writeStream);
} else if (buildType === "paketo_buildpacks") {
await buildPaketo(application, writeStream);
} else if (buildType === "dockerfile") {
await buildCustomDocker(application, writeStream);
} else if (buildType === "static") {
await buildStatic(application, writeStream);
} else if (buildType === "railpack") {
await buildRailpack(application, writeStream);
}
if (application.registryId) {
await uploadImage(application, writeStream);
}
await mechanizeDockerContainer(application);
writeStream.write("Docker Deployed: ✅");
} catch (error) {
if (error instanceof Error) {
writeStream.write(`Error ❌\n${error?.message}`);
} else {
writeStream.write("Error ❌");
}
throw error;
} finally {
writeStream.end();
}
};
export const getBuildCommand = (
application: ApplicationNested,
logPath: string,
) => {
let command = ""; let command = "";
const { buildType, registry } = application; const { buildType, registry } = application;
if (application.sourceType === "docker") {
return "";
}
switch (buildType) { switch (buildType) {
case "nixpacks": case "nixpacks":
command = getNixpacksCommand(application, logPath); command = getNixpacksCommand(application);
break; break;
case "heroku_buildpacks": case "heroku_buildpacks":
command = getHerokuCommand(application, logPath); command = getHerokuCommand(application);
break; break;
case "paketo_buildpacks": case "paketo_buildpacks":
command = getPaketoCommand(application, logPath); command = getPaketoCommand(application);
break; break;
case "static": case "static":
command = getStaticCommand(application, logPath); command = getStaticCommand(application);
break; break;
case "dockerfile": case "dockerfile":
command = getDockerCommand(application, logPath); command = getDockerCommand(application);
break; break;
case "railpack": case "railpack":
command = getRailpackCommand(application, logPath); command = getRailpackCommand(application);
break; break;
} }
if (registry) { if (registry) {
command += uploadImageRemoteCommand(application, logPath); command += uploadImageRemoteCommand(application);
} }
return command; return command;
@@ -143,6 +101,7 @@ export const mechanizeDockerContainer = async (
UpdateConfig, UpdateConfig,
Networks, Networks,
StopGracePeriod, StopGracePeriod,
EndpointSpec,
} = generateConfigContainer(application); } = generateConfigContainer(application);
const bindsMount = generateBindMounts(mounts); const bindsMount = generateBindMounts(mounts);
@@ -183,14 +142,16 @@ export const mechanizeDockerContainer = async (
}, },
Mode, Mode,
RollbackConfig, RollbackConfig,
EndpointSpec: { EndpointSpec: EndpointSpec
Ports: ports.map((port) => ({ ? EndpointSpec
PublishMode: port.publishMode, : {
Protocol: port.protocol, Ports: ports.map((port) => ({
TargetPort: port.targetPort, PublishMode: port.publishMode,
PublishedPort: port.publishedPort, Protocol: port.protocol,
})), TargetPort: port.targetPort,
}, PublishedPort: port.publishedPort,
})),
},
UpdateConfig, UpdateConfig,
...(StopGracePeriod !== undefined && ...(StopGracePeriod !== undefined &&
StopGracePeriod !== null && { StopGracePeriod }), StopGracePeriod !== null && { StopGracePeriod }),
+17 -107
View File
@@ -1,101 +1,11 @@
import { existsSync, mkdirSync, type WriteStream } from "node:fs";
import path from "node:path"; import path from "node:path";
import { import { getStaticCommand } from "@dokploy/server/utils/builders/static";
buildStatic,
getStaticCommand,
} from "@dokploy/server/utils/builders/static";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { prepareEnvironmentVariables } from "../docker/utils"; import { prepareEnvironmentVariables } from "../docker/utils";
import { getBuildAppDirectory } from "../filesystem/directory"; import { getBuildAppDirectory } from "../filesystem/directory";
import { spawnAsync } from "../process/spawnAsync";
import type { ApplicationNested } from "."; import type { ApplicationNested } from ".";
export const buildNixpacks = async ( export const getNixpacksCommand = (application: ApplicationNested) => {
application: ApplicationNested,
writeStream: WriteStream,
) => {
const { env, appName, publishDirectory, cleanCache } = application;
const buildAppDirectory = getBuildAppDirectory(application);
const buildContainerId = `${appName}-${nanoid(10)}`;
const envVariables = prepareEnvironmentVariables(
env,
application.environment.project.env,
application.environment.env,
);
const writeToStream = (data: string) => {
if (writeStream.writable) {
writeStream.write(data);
}
};
try {
const args = ["build", buildAppDirectory, "--name", appName];
if (cleanCache) {
args.push("--no-cache");
}
for (const env of envVariables) {
args.push("--env", env);
}
if (publishDirectory) {
/* No need for any start command, since we'll use nginx later on */
args.push("--no-error-without-start");
}
await spawnAsync("nixpacks", args, writeToStream);
/*
Run the container with the image created by nixpacks,
and copy the artifacts on the host filesystem.
Then, remove the container and create a static build.
*/
if (publishDirectory) {
await spawnAsync(
"docker",
["create", "--name", buildContainerId, appName],
writeToStream,
);
const localPath = path.join(buildAppDirectory, publishDirectory);
if (!existsSync(path.dirname(localPath))) {
mkdirSync(path.dirname(localPath), { recursive: true });
}
// https://docs.docker.com/reference/cli/docker/container/cp/
const isDirectory =
publishDirectory.endsWith("/") || !path.extname(publishDirectory);
await spawnAsync(
"docker",
[
"cp",
`${buildContainerId}:/app/${publishDirectory}${isDirectory ? "/." : ""}`,
localPath,
],
writeToStream,
);
await spawnAsync("docker", ["rm", buildContainerId], writeToStream);
await buildStatic(application, writeStream);
}
return true;
} catch (e) {
await spawnAsync("docker", ["rm", buildContainerId], writeToStream);
throw e;
}
};
export const getNixpacksCommand = (
application: ApplicationNested,
logPath: string,
) => {
const { env, appName, publishDirectory, cleanCache } = application; const { env, appName, publishDirectory, cleanCache } = application;
const buildAppDirectory = getBuildAppDirectory(application); const buildAppDirectory = getBuildAppDirectory(application);
@@ -122,12 +32,12 @@ export const getNixpacksCommand = (
} }
const command = `nixpacks ${args.join(" ")}`; const command = `nixpacks ${args.join(" ")}`;
let bashCommand = ` let bashCommand = `
echo "Starting nixpacks build..." >> ${logPath}; echo "Starting nixpacks build..." ;
${command} >> ${logPath} 2>> ${logPath} || { ${command} || {
echo "❌ Nixpacks build failed" >> ${logPath}; echo "❌ Nixpacks build failed" ;
exit 1; exit 1;
} }
echo "✅ Nixpacks build completed." >> ${logPath}; echo "✅ Nixpacks build completed." ;
`; `;
/* /*
@@ -141,16 +51,16 @@ echo "✅ Nixpacks build completed." >> ${logPath};
publishDirectory.endsWith("/") || !path.extname(publishDirectory); publishDirectory.endsWith("/") || !path.extname(publishDirectory);
bashCommand += ` bashCommand += `
docker create --name ${buildContainerId} ${appName} docker create --name ${buildContainerId} ${appName}
mkdir -p ${localPath} mkdir -p ${localPath}
docker cp ${buildContainerId}:/app/${publishDirectory}${isDirectory ? "/." : ""} ${path.join(buildAppDirectory, publishDirectory)} >> ${logPath} 2>> ${logPath} || { docker cp ${buildContainerId}:/app/${publishDirectory}${isDirectory ? "/." : ""} ${path.join(buildAppDirectory, publishDirectory)} || {
docker rm ${buildContainerId}
echo "❌ Copying ${publishDirectory} to ${path.join(buildAppDirectory, publishDirectory)} failed" ;
exit 1;
}
docker rm ${buildContainerId} docker rm ${buildContainerId}
echo "❌ Copying ${publishDirectory} to ${path.join(buildAppDirectory, publishDirectory)} failed" >> ${logPath}; ${getStaticCommand(application)}
exit 1; `;
}
docker rm ${buildContainerId}
${getStaticCommand(application, logPath)}
`;
} }
return bashCommand; return bashCommand;
+5 -50
View File
@@ -1,53 +1,8 @@
import type { WriteStream } from "node:fs";
import { prepareEnvironmentVariables } from "../docker/utils"; import { prepareEnvironmentVariables } from "../docker/utils";
import { getBuildAppDirectory } from "../filesystem/directory"; import { getBuildAppDirectory } from "../filesystem/directory";
import { spawnAsync } from "../process/spawnAsync";
import type { ApplicationNested } from "."; import type { ApplicationNested } from ".";
export const buildPaketo = async ( export const getPaketoCommand = (application: ApplicationNested) => {
application: ApplicationNested,
writeStream: WriteStream,
) => {
const { env, appName, cleanCache } = application;
const buildAppDirectory = getBuildAppDirectory(application);
const envVariables = prepareEnvironmentVariables(
env,
application.environment.project.env,
application.environment.env,
);
try {
const args = [
"build",
appName,
"--path",
buildAppDirectory,
"--builder",
"paketobuildpacks/builder-jammy-full",
];
if (cleanCache) {
args.push("--clear-cache");
}
for (const env of envVariables) {
args.push("--env", env);
}
await spawnAsync("pack", args, (data) => {
if (writeStream.writable) {
writeStream.write(data);
}
});
return true;
} catch (e) {
throw e;
}
};
export const getPaketoCommand = (
application: ApplicationNested,
logPath: string,
) => {
const { env, appName, cleanCache } = application; const { env, appName, cleanCache } = application;
const buildAppDirectory = getBuildAppDirectory(application); const buildAppDirectory = getBuildAppDirectory(application);
@@ -76,12 +31,12 @@ export const getPaketoCommand = (
const command = `pack ${args.join(" ")}`; const command = `pack ${args.join(" ")}`;
const bashCommand = ` const bashCommand = `
echo "Starting Paketo build..." >> ${logPath}; echo "Starting Paketo build..." ;
${command} >> ${logPath} 2>> ${logPath} || { ${command} || {
echo "❌ Paketo build failed" >> ${logPath}; echo "❌ Paketo build failed" ;
exit 1; exit 1;
} }
echo "✅ Paketo build completed." >> ${logPath}; echo "✅ Paketo build completed." ;
`; `;
return bashCommand; return bashCommand;
+12 -113
View File
@@ -1,13 +1,10 @@
import { createHash } from "node:crypto"; import { createHash } from "node:crypto";
import type { WriteStream } from "node:fs";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { import {
parseEnvironmentKeyValuePair, parseEnvironmentKeyValuePair,
prepareEnvironmentVariables, prepareEnvironmentVariables,
} from "../docker/utils"; } from "../docker/utils";
import { getBuildAppDirectory } from "../filesystem/directory"; import { getBuildAppDirectory } from "../filesystem/directory";
import { execAsync } from "../process/execAsync";
import { spawnAsync } from "../process/spawnAsync";
import type { ApplicationNested } from "."; import type { ApplicationNested } from ".";
const calculateSecretsHash = (envVariables: string[]): string => { const calculateSecretsHash = (envVariables: string[]): string => {
@@ -18,108 +15,7 @@ const calculateSecretsHash = (envVariables: string[]): string => {
return hash.digest("hex"); return hash.digest("hex");
}; };
export const buildRailpack = async ( export const getRailpackCommand = (application: ApplicationNested) => {
application: ApplicationNested,
writeStream: WriteStream,
) => {
const { env, appName, cleanCache } = application;
const buildAppDirectory = getBuildAppDirectory(application);
const envVariables = prepareEnvironmentVariables(
env,
application.environment.project.env,
application.environment.env,
);
try {
await execAsync(
"docker buildx create --use --name builder-containerd --driver docker-container || true",
);
await execAsync("docker buildx use builder-containerd");
// First prepare the build plan and info
const prepareArgs = [
"prepare",
buildAppDirectory,
"--plan-out",
`${buildAppDirectory}/railpack-plan.json`,
"--info-out",
`${buildAppDirectory}/railpack-info.json`,
];
// Add environment variables to prepare command
for (const env of envVariables) {
prepareArgs.push("--env", env);
}
// Run prepare command
await spawnAsync("railpack", prepareArgs, (data) => {
if (writeStream.writable) {
writeStream.write(data);
}
});
// Calculate secrets hash for layer invalidation
const secretsHash = calculateSecretsHash(envVariables);
// Build with BuildKit using the Railpack frontend
const cacheKey = cleanCache ? nanoid(10) : undefined;
const buildArgs = [
"buildx",
"build",
...(cacheKey
? [
"--build-arg",
`secrets-hash=${secretsHash}`,
"--build-arg",
`cache-key=${cacheKey}`,
]
: []),
"--build-arg",
`BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v${application.railpackVersion}`,
"-f",
`${buildAppDirectory}/railpack-plan.json`,
"--output",
`type=docker,name=${appName}`,
];
// Add secrets properly formatted
const env: { [key: string]: string } = {};
for (const pair of envVariables) {
const [key, value] = parseEnvironmentKeyValuePair(pair);
if (key && value) {
buildArgs.push("--secret", `id=${key},env=${key}`);
env[key] = value;
}
}
buildArgs.push(buildAppDirectory);
await spawnAsync(
"docker",
buildArgs,
(data) => {
if (writeStream.writable) {
writeStream.write(data);
}
},
{
env: { ...process.env, ...env },
},
);
return true;
} catch (e) {
throw e;
} finally {
await execAsync("docker buildx rm builder-containerd");
}
};
export const getRailpackCommand = (
application: ApplicationNested,
logPath: string,
) => {
const { env, appName, cleanCache } = application; const { env, appName, cleanCache } = application;
const buildAppDirectory = getBuildAppDirectory(application); const buildAppDirectory = getBuildAppDirectory(application);
const envVariables = prepareEnvironmentVariables( const envVariables = prepareEnvironmentVariables(
@@ -179,25 +75,28 @@ export const getRailpackCommand = (
buildArgs.push(buildAppDirectory); buildArgs.push(buildAppDirectory);
const bashCommand = ` const bashCommand = `
# Ensure we have a builder with containerd # Ensure we have a builder with containerd
docker buildx create --use --name builder-containerd --driver docker-container || true docker buildx create --use --name builder-containerd --driver docker-container || true
docker buildx use builder-containerd docker buildx use builder-containerd
echo "Preparing Railpack build plan..." >> "${logPath}"; echo "Preparing Railpack build plan..." ;
railpack ${prepareArgs.join(" ")} >> ${logPath} 2>> ${logPath} || { railpack ${prepareArgs.join(" ")} || {
echo "❌ Railpack prepare failed" >> ${logPath}; echo "❌ Railpack prepare failed" ;
docker buildx rm builder-containerd || true
exit 1; exit 1;
} }
echo "✅ Railpack prepare completed." >> ${logPath}; echo "✅ Railpack prepare completed." ;
echo "Building with Railpack frontend..." >> "${logPath}"; echo "Building with Railpack frontend..." ;
# Export environment variables for secrets # Export environment variables for secrets
${exportEnvs.join("\n")} ${exportEnvs.join("\n")}
docker ${buildArgs.join(" ")} >> ${logPath} 2>> ${logPath} || { docker ${buildArgs.join(" ")} || {
echo "❌ Railpack build failed" >> ${logPath}; echo "❌ Railpack build failed" ;
docker buildx rm builder-containerd || true
exit 1; exit 1;
} }
echo "✅ Railpack build completed." >> ${logPath}; echo "✅ Railpack build completed." ;
docker buildx rm builder-containerd docker buildx rm builder-containerd
`; `;
+21 -66
View File
@@ -1,9 +1,5 @@
import type { WriteStream } from "node:fs"; import { getDockerCommand } from "@dokploy/server/utils/builders/docker-file";
import { import { getCreateFileCommand } from "../docker/utils";
buildCustomDocker,
getDockerCommand,
} from "@dokploy/server/utils/builders/docker-file";
import { createFile, getCreateFileCommand } from "../docker/utils";
import { getBuildAppDirectory } from "../filesystem/directory"; import { getBuildAppDirectory } from "../filesystem/directory";
import type { ApplicationNested } from "."; import type { ApplicationNested } from ".";
@@ -32,81 +28,40 @@ http {
} }
`; `;
export const buildStatic = async ( export const getStaticCommand = (application: ApplicationNested) => {
application: ApplicationNested,
writeStream: WriteStream,
) => {
const { publishDirectory, isStaticSpa } = application; const { publishDirectory, isStaticSpa } = application;
const buildAppDirectory = getBuildAppDirectory(application); const buildAppDirectory = getBuildAppDirectory(application);
let command = "";
try { if (isStaticSpa) {
if (isStaticSpa) { command += getCreateFileCommand(
createFile(buildAppDirectory, "nginx.conf", nginxSpaConfig);
}
createFile(
buildAppDirectory, buildAppDirectory,
".dockerignore", "nginx.conf",
[".git", ".env", "Dockerfile", ".dockerignore"].join("\n"), nginxSpaConfig,
); );
createFile(
buildAppDirectory,
"Dockerfile",
[
"FROM nginx:alpine",
"WORKDIR /usr/share/nginx/html/",
isStaticSpa ? "COPY nginx.conf /etc/nginx/nginx.conf" : "",
`COPY ${publishDirectory || "."} .`,
'CMD ["nginx", "-g", "daemon off;"]',
].join("\n"),
);
createFile(
buildAppDirectory,
".dockerignore",
[".git", ".env", "Dockerfile", ".dockerignore"].join("\n"),
);
await buildCustomDocker(
{
...application,
buildType: "dockerfile",
dockerfile: "Dockerfile",
},
writeStream,
);
return true;
} catch (e) {
throw e;
} }
};
export const getStaticCommand = ( command += getCreateFileCommand(
application: ApplicationNested, buildAppDirectory,
logPath: string, ".dockerignore",
) => { [".git", ".env", "Dockerfile", ".dockerignore"].join("\n"),
const { publishDirectory } = application; );
const buildAppDirectory = getBuildAppDirectory(application);
let command = getCreateFileCommand( command += getCreateFileCommand(
buildAppDirectory, buildAppDirectory,
"Dockerfile", "Dockerfile",
[ [
"FROM nginx:alpine", "FROM nginx:alpine",
"WORKDIR /usr/share/nginx/html/", "WORKDIR /usr/share/nginx/html/",
isStaticSpa ? "COPY nginx.conf /etc/nginx/nginx.conf" : "",
`COPY ${publishDirectory || "."} .`, `COPY ${publishDirectory || "."} .`,
'CMD ["nginx", "-g", "daemon off;"]',
].join("\n"), ].join("\n"),
); );
command += getDockerCommand( command += getDockerCommand({
{ ...application,
...application, buildType: "dockerfile",
buildType: "dockerfile", dockerfile: "Dockerfile",
dockerfile: "Dockerfile", });
},
logPath,
);
return command; return command;
}; };
+11 -73
View File
@@ -1,11 +1,6 @@
import type { WriteStream } from "node:fs";
import type { ApplicationNested } from "../builders"; import type { ApplicationNested } from "../builders";
import { spawnAsync } from "../process/spawnAsync";
export const uploadImage = async ( export const uploadImageRemoteCommand = (application: ApplicationNested) => {
application: ApplicationNested,
writeStream: WriteStream,
) => {
const registry = application.registry; const registry = application.registry;
if (!registry) { if (!registry) {
@@ -19,85 +14,28 @@ export const uploadImage = async (
const finalURL = registryUrl; const finalURL = registryUrl;
// Build registry tag in correct format: registry.com/owner/image:tag // Build registry tag in correct format: registry.com/owner/image:tag
// For ghcr.io: ghcr.io/username/image:tag
// For docker.io: docker.io/username/image:tag
const registryTag = imagePrefix const registryTag = imagePrefix
? `${registryUrl ? `${registryUrl}/` : ""}${imagePrefix}/${imageName}` ? `${registryUrl ? `${registryUrl}/` : ""}${imagePrefix}/${imageName}`
: `${registryUrl ? `${registryUrl}/` : ""}${username}/${imageName}`; : `${registryUrl ? `${registryUrl}/` : ""}${username}/${imageName}`;
try {
writeStream.write(
`📦 [Enabled Registry] Uploading image to ${registry.registryType} | ${imageName} | ${finalURL} | ${registryTag}\n`,
);
const loginCommand = spawnAsync(
"docker",
["login", finalURL, "-u", registry.username, "--password-stdin"],
(data) => {
if (writeStream.writable) {
writeStream.write(data);
}
},
);
loginCommand.child?.stdin?.write(registry.password);
loginCommand.child?.stdin?.end();
await loginCommand;
await spawnAsync("docker", ["tag", imageName, registryTag], (data) => {
if (writeStream.writable) {
writeStream.write(data);
}
});
await spawnAsync("docker", ["push", registryTag], (data) => {
if (writeStream.writable) {
writeStream.write(data);
}
});
} catch (error) {
console.log(error);
throw error;
}
};
export const uploadImageRemoteCommand = (
application: ApplicationNested,
logPath: string,
) => {
const registry = application.registry;
if (!registry) {
throw new Error("Registry not found");
}
const { registryUrl, imagePrefix, username } = registry;
const { appName } = application;
const imageName = `${appName}:latest`;
const finalURL = registryUrl;
// Build registry tag in correct format: registry.com/owner/image:tag
const registryTag = imagePrefix
? `${registryUrl}/${imagePrefix}/${imageName}`
: `${registryUrl}/${username}/${imageName}`;
try { try {
const command = ` const command = `
echo "📦 [Enabled Registry] Uploading image to '${registry.registryType}' | '${registryTag}'" >> ${logPath}; echo "📦 [Enabled Registry] Uploading image to '${registry.registryType}' | '${registryTag}'" ;
echo "${registry.password}" | docker login ${finalURL} -u ${registry.username} --password-stdin >> ${logPath} 2>> ${logPath} || { echo "${registry.password}" | docker login ${finalURL} -u ${registry.username} --password-stdin || {
echo "❌ DockerHub Failed" >> ${logPath}; echo "❌ DockerHub Failed" ;
exit 1; exit 1;
} }
echo "✅ Registry Login Success" >> ${logPath}; echo "✅ Registry Login Success" ;
docker tag ${imageName} ${registryTag} >> ${logPath} 2>> ${logPath} || { docker tag ${imageName} ${registryTag} || {
echo "❌ Error tagging image" >> ${logPath}; echo "❌ Error tagging image" ;
exit 1; exit 1;
} }
echo "✅ Image Tagged" >> ${logPath}; echo "✅ Image Tagged" ;
docker push ${registryTag} 2>> ${logPath} || { docker push ${registryTag} || {
echo "❌ Error pushing image" >> ${logPath}; echo "❌ Error pushing image" ;
exit 1; exit 1;
} }
echo "✅ Image Pushed" >> ${logPath}; echo "✅ Image Pushed" ;
`; `;
return command; return command;
} catch (error) { } catch (error) {
+16 -13
View File
@@ -46,6 +46,7 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
UpdateConfig, UpdateConfig,
Networks, Networks,
StopGracePeriod, StopGracePeriod,
EndpointSpec,
} = generateConfigContainer(mariadb); } = generateConfigContainer(mariadb);
const resources = calculateResources({ const resources = calculateResources({
memoryLimit, memoryLimit,
@@ -89,19 +90,21 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
}, },
Mode, Mode,
RollbackConfig, RollbackConfig,
EndpointSpec: { EndpointSpec: EndpointSpec
Mode: "dnsrr", ? EndpointSpec
Ports: externalPort : {
? [ Mode: "dnsrr" as const,
{ Ports: externalPort
Protocol: "tcp", ? [
TargetPort: 3306, {
PublishedPort: externalPort, Protocol: "tcp" as const,
PublishMode: "host", TargetPort: 3306,
}, PublishedPort: externalPort,
] PublishMode: "host" as const,
: [], },
}, ]
: [],
},
UpdateConfig, UpdateConfig,
...(StopGracePeriod !== undefined && ...(StopGracePeriod !== undefined &&
StopGracePeriod !== null && { StopGracePeriod }), StopGracePeriod !== null && { StopGracePeriod }),
+16 -13
View File
@@ -92,6 +92,7 @@ ${command ?? "wait $MONGOD_PID"}`;
UpdateConfig, UpdateConfig,
Networks, Networks,
StopGracePeriod, StopGracePeriod,
EndpointSpec,
} = generateConfigContainer(mongo); } = generateConfigContainer(mongo);
const resources = calculateResources({ const resources = calculateResources({
@@ -142,19 +143,21 @@ ${command ?? "wait $MONGOD_PID"}`;
}, },
Mode, Mode,
RollbackConfig, RollbackConfig,
EndpointSpec: { EndpointSpec: EndpointSpec
Mode: "dnsrr", ? EndpointSpec
Ports: externalPort : {
? [ Mode: "dnsrr" as const,
{ Ports: externalPort
Protocol: "tcp", ? [
TargetPort: 27017, {
PublishedPort: externalPort, Protocol: "tcp" as const,
PublishMode: "host", TargetPort: 27017,
}, PublishedPort: externalPort,
] PublishMode: "host" as const,
: [], },
}, ]
: [],
},
UpdateConfig, UpdateConfig,
...(StopGracePeriod !== undefined && ...(StopGracePeriod !== undefined &&
StopGracePeriod !== null && { StopGracePeriod }), StopGracePeriod !== null && { StopGracePeriod }),
+16 -13
View File
@@ -52,6 +52,7 @@ export const buildMysql = async (mysql: MysqlNested) => {
UpdateConfig, UpdateConfig,
Networks, Networks,
StopGracePeriod, StopGracePeriod,
EndpointSpec,
} = generateConfigContainer(mysql); } = generateConfigContainer(mysql);
const resources = calculateResources({ const resources = calculateResources({
memoryLimit, memoryLimit,
@@ -95,19 +96,21 @@ export const buildMysql = async (mysql: MysqlNested) => {
}, },
Mode, Mode,
RollbackConfig, RollbackConfig,
EndpointSpec: { EndpointSpec: EndpointSpec
Mode: "dnsrr", ? EndpointSpec
Ports: externalPort : {
? [ Mode: "dnsrr" as const,
{ Ports: externalPort
Protocol: "tcp", ? [
TargetPort: 3306, {
PublishedPort: externalPort, Protocol: "tcp" as const,
PublishMode: "host", TargetPort: 3306,
}, PublishedPort: externalPort,
] PublishMode: "host" as const,
: [], },
}, ]
: [],
},
UpdateConfig, UpdateConfig,
...(StopGracePeriod !== undefined && ...(StopGracePeriod !== undefined &&
StopGracePeriod !== null && { StopGracePeriod }), StopGracePeriod !== null && { StopGracePeriod }),
+16 -13
View File
@@ -45,6 +45,7 @@ export const buildPostgres = async (postgres: PostgresNested) => {
UpdateConfig, UpdateConfig,
Networks, Networks,
StopGracePeriod, StopGracePeriod,
EndpointSpec,
} = generateConfigContainer(postgres); } = generateConfigContainer(postgres);
const resources = calculateResources({ const resources = calculateResources({
memoryLimit, memoryLimit,
@@ -88,19 +89,21 @@ export const buildPostgres = async (postgres: PostgresNested) => {
}, },
Mode, Mode,
RollbackConfig, RollbackConfig,
EndpointSpec: { EndpointSpec: EndpointSpec
Mode: "dnsrr", ? EndpointSpec
Ports: externalPort : {
? [ Mode: "dnsrr" as const,
{ Ports: externalPort
Protocol: "tcp", ? [
TargetPort: 5432, {
PublishedPort: externalPort, Protocol: "tcp" as const,
PublishMode: "host", TargetPort: 5432,
}, PublishedPort: externalPort,
] PublishMode: "host" as const,
: [], },
}, ]
: [],
},
UpdateConfig, UpdateConfig,
...(StopGracePeriod !== undefined && ...(StopGracePeriod !== undefined &&
StopGracePeriod !== null && { StopGracePeriod }), StopGracePeriod !== null && { StopGracePeriod }),
+16 -13
View File
@@ -43,6 +43,7 @@ export const buildRedis = async (redis: RedisNested) => {
UpdateConfig, UpdateConfig,
Networks, Networks,
StopGracePeriod, StopGracePeriod,
EndpointSpec,
} = generateConfigContainer(redis); } = generateConfigContainer(redis);
const resources = calculateResources({ const resources = calculateResources({
memoryLimit, memoryLimit,
@@ -85,19 +86,21 @@ export const buildRedis = async (redis: RedisNested) => {
}, },
Mode, Mode,
RollbackConfig, RollbackConfig,
EndpointSpec: { EndpointSpec: EndpointSpec
Mode: "dnsrr", ? EndpointSpec
Ports: externalPort : {
? [ Mode: "dnsrr" as const,
{ Ports: externalPort
Protocol: "tcp", ? [
TargetPort: 6379, {
PublishedPort: externalPort, Protocol: "tcp" as const,
PublishMode: "host", TargetPort: 6379,
}, PublishedPort: externalPort,
] PublishMode: "host" as const,
: [], },
}, ]
: [],
},
UpdateConfig, UpdateConfig,
...(StopGracePeriod !== undefined && ...(StopGracePeriod !== undefined &&
StopGracePeriod !== null && { StopGracePeriod }), StopGracePeriod !== null && { StopGracePeriod }),
@@ -1,11 +1,11 @@
import { findComposeById } from "@dokploy/server/services/compose"; import { findComposeById } from "@dokploy/server/services/compose";
import { stringify } from "yaml"; import { stringify } from "yaml";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { addAppNameToAllServiceNames } from "./collision/root-network"; import { addAppNameToAllServiceNames } from "./collision/root-network";
import { generateRandomHash } from "./compose"; import { generateRandomHash } from "./compose";
import { addSuffixToAllVolumes } from "./compose/volume"; import { addSuffixToAllVolumes } from "./compose/volume";
import { import {
cloneCompose, cloneCompose,
cloneComposeRemote,
loadDockerCompose, loadDockerCompose,
loadDockerComposeRemote, loadDockerComposeRemote,
} from "./domain"; } from "./domain";
@@ -31,10 +31,11 @@ export const randomizeIsolatedDeploymentComposeFile = async (
) => { ) => {
const compose = await findComposeById(composeId); const compose = await findComposeById(composeId);
const command = await cloneCompose(compose);
if (compose.serverId) { if (compose.serverId) {
await cloneComposeRemote(compose); await execAsyncRemote(compose.serverId, command);
} else { } else {
await cloneCompose(compose); await execAsync(command);
} }
let composeData: ComposeSpecification | null; let composeData: ComposeSpecification | null;
+23 -70
View File
@@ -1,35 +1,16 @@
import fs, { existsSync, readFileSync } from "node:fs"; import fs, { existsSync, readFileSync } from "node:fs";
import { writeFile } from "node:fs/promises";
import { join } from "node:path"; import { join } from "node:path";
import { paths } from "@dokploy/server/constants"; import { paths } from "@dokploy/server/constants";
import type { Compose } from "@dokploy/server/services/compose"; import type { Compose } from "@dokploy/server/services/compose";
import type { Domain } from "@dokploy/server/services/domain"; import type { Domain } from "@dokploy/server/services/domain";
import { parse, stringify } from "yaml"; import { parse, stringify } from "yaml";
import { execAsyncRemote } from "../process/execAsync"; import { execAsyncRemote } from "../process/execAsync";
import { import { cloneBitbucketRepository } from "../providers/bitbucket";
cloneRawBitbucketRepository, import { cloneGitRepository } from "../providers/git";
cloneRawBitbucketRepositoryRemote, import { cloneGiteaRepository } from "../providers/gitea";
} from "../providers/bitbucket"; import { cloneGithubRepository } from "../providers/github";
import { import { cloneGitlabRepository } from "../providers/gitlab";
cloneGitRawRepository, import { getCreateComposeFileCommand } from "../providers/raw";
cloneRawGitRepositoryRemote,
} from "../providers/git";
import {
cloneRawGiteaRepository,
cloneRawGiteaRepositoryRemote,
} from "../providers/gitea";
import {
cloneRawGithubRepository,
cloneRawGithubRepositoryRemote,
} from "../providers/github";
import {
cloneRawGitlabRepository,
cloneRawGitlabRepositoryRemote,
} from "../providers/gitlab";
import {
createComposeFileRaw,
createComposeFileRawRemote,
} from "../providers/raw";
import { randomizeDeployableSpecificationFile } from "./collision"; import { randomizeDeployableSpecificationFile } from "./collision";
import { randomizeSpecificationFile } from "./compose"; import { randomizeSpecificationFile } from "./compose";
import type { import type {
@@ -40,35 +21,25 @@ import type {
import { encodeBase64 } from "./utils"; import { encodeBase64 } from "./utils";
export const cloneCompose = async (compose: Compose) => { export const cloneCompose = async (compose: Compose) => {
let command = "set -e;";
const entity = {
...compose,
type: "compose" as const,
};
if (compose.sourceType === "github") { if (compose.sourceType === "github") {
await cloneRawGithubRepository(compose); command += await cloneGithubRepository(entity);
} else if (compose.sourceType === "gitlab") { } else if (compose.sourceType === "gitlab") {
await cloneRawGitlabRepository(compose); command += await cloneGitlabRepository(entity);
} else if (compose.sourceType === "bitbucket") { } else if (compose.sourceType === "bitbucket") {
await cloneRawBitbucketRepository(compose); command += await cloneBitbucketRepository(entity);
} else if (compose.sourceType === "git") { } else if (compose.sourceType === "git") {
await cloneGitRawRepository(compose); command += await cloneGitRepository(entity);
} else if (compose.sourceType === "gitea") { } else if (compose.sourceType === "gitea") {
await cloneRawGiteaRepository(compose); command += await cloneGiteaRepository(entity);
} else if (compose.sourceType === "raw") { } else if (compose.sourceType === "raw") {
await createComposeFileRaw(compose); command += getCreateComposeFileCommand(compose);
}
};
export const cloneComposeRemote = async (compose: Compose) => {
if (compose.sourceType === "github") {
await cloneRawGithubRepositoryRemote(compose);
} else if (compose.sourceType === "gitlab") {
await cloneRawGitlabRepositoryRemote(compose);
} else if (compose.sourceType === "bitbucket") {
await cloneRawBitbucketRepositoryRemote(compose);
} else if (compose.sourceType === "git") {
await cloneRawGitRepositoryRemote(compose);
} else if (compose.sourceType === "gitea") {
await cloneRawGiteaRepositoryRemote(compose);
} else if (compose.sourceType === "raw") {
await createComposeFileRawRemote(compose);
} }
return command;
}; };
export const getComposePath = (compose: Compose) => { export const getComposePath = (compose: Compose) => {
@@ -131,28 +102,9 @@ export const readComposeFile = async (compose: Compose) => {
return null; return null;
}; };
export const writeDomainsToCompose = async (
compose: Compose,
domains: Domain[],
) => {
if (!domains.length) {
return;
}
const composeConverted = await addDomainToCompose(compose, domains);
const path = getComposePath(compose);
const composeString = stringify(composeConverted, { lineWidth: 1000 });
try {
await writeFile(path, composeString, "utf8");
} catch (error) {
throw error;
}
};
export const writeDomainsToComposeRemote = async ( export const writeDomainsToComposeRemote = async (
compose: Compose, compose: Compose,
domains: Domain[], domains: Domain[],
logPath: string,
) => { ) => {
if (!domains.length) { if (!domains.length) {
return ""; return "";
@@ -164,7 +116,7 @@ export const writeDomainsToComposeRemote = async (
if (!composeConverted) { if (!composeConverted) {
return ` return `
echo "❌ Error: Compose file not found" >> ${logPath}; echo "❌ Error: Compose file not found";
exit 1; exit 1;
`; `;
} }
@@ -175,12 +127,13 @@ exit 1;
} }
} catch (error) { } catch (error) {
// @ts-ignore // @ts-ignore
return `echo "❌ Has occured an error: ${error?.message || error}" >> ${logPath}; return `echo "❌ Has occured an error: ${error?.message || error}";
exit 1; exit 1;
`; `;
} }
return "";
}; };
// (node:59875) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 SIGTERM listeners added to [process]. Use emitter.setMaxListeners() to increase limit
export const addDomainToCompose = async ( export const addDomainToCompose = async (
compose: Compose, compose: Compose,
domains: Domain[], domains: Domain[],
@@ -190,7 +143,7 @@ export const addDomainToCompose = async (
let result: ComposeSpecification | null; let result: ComposeSpecification | null;
if (compose.serverId) { if (compose.serverId) {
result = await loadDockerComposeRemote(compose); // aca hay que ir al servidor e ir a traer el compose file al servidor result = await loadDockerComposeRemote(compose);
} else { } else {
result = await loadDockerCompose(compose); result = await loadDockerCompose(compose);
} }
+16 -5
View File
@@ -395,6 +395,7 @@ export const generateConfigContainer = (
mounts, mounts,
networkSwarm, networkSwarm,
stopGracePeriodSwarm, stopGracePeriodSwarm,
endpointSpecSwarm,
} = application; } = application;
const sanitizedStopGracePeriodSwarm = const sanitizedStopGracePeriodSwarm =
@@ -408,11 +409,9 @@ export const generateConfigContainer = (
...(healthCheckSwarm && { ...(healthCheckSwarm && {
HealthCheck: healthCheckSwarm, HealthCheck: healthCheckSwarm,
}), }),
...(restartPolicySwarm ...(restartPolicySwarm && {
? { RestartPolicy: restartPolicySwarm,
RestartPolicy: restartPolicySwarm, }),
}
: {}),
...(placementSwarm ...(placementSwarm
? { ? {
Placement: placementSwarm, Placement: placementSwarm,
@@ -461,6 +460,18 @@ export const generateConfigContainer = (
: { : {
Networks: [{ Target: "dokploy-network" }], Networks: [{ Target: "dokploy-network" }],
}), }),
...(endpointSpecSwarm && {
EndpointSpec: {
...(endpointSpecSwarm.Mode && { Mode: endpointSpecSwarm.Mode }),
Ports:
endpointSpecSwarm.Ports?.map((port) => ({
Protocol: (port.Protocol || "tcp") as "tcp" | "udp" | "sctp",
TargetPort: port.TargetPort || 0,
PublishedPort: port.PublishedPort || 0,
PublishMode: (port.PublishMode || "host") as "ingress" | "host",
})) || [],
},
}),
}; };
}; };
@@ -116,11 +116,7 @@ export const execAsyncRemote = async (
if (code === 0) { if (code === 0) {
resolve({ stdout, stderr }); resolve({ stdout, stderr });
} else { } else {
reject( reject(new Error(`Error occurred ❌: ${stderr}`));
new Error(
`Command exited with code ${code}. Stderr: ${stderr}, command: ${command}`,
),
);
} }
}) })
.on("data", (data: string) => { .on("data", (data: string) => {
+31 -186
View File
@@ -1,4 +1,3 @@
import { createWriteStream } from "node:fs";
import { join } from "node:path"; import { join } from "node:path";
import { paths } from "@dokploy/server/constants"; import { paths } from "@dokploy/server/constants";
import type { import type {
@@ -9,12 +8,8 @@ import {
type Bitbucket, type Bitbucket,
findBitbucketById, findBitbucketById,
} from "@dokploy/server/services/bitbucket"; } from "@dokploy/server/services/bitbucket";
import type { Compose } from "@dokploy/server/services/compose";
import type { InferResultType } from "@dokploy/server/types/with"; import type { InferResultType } from "@dokploy/server/types/with";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { recreateDirectory } from "../filesystem/directory";
import { execAsyncRemote } from "../process/execAsync";
import { spawnAsync } from "../process/spawnAsync";
export type ApplicationWithBitbucket = InferResultType< export type ApplicationWithBitbucket = InferResultType<
"applications", "applications",
@@ -81,202 +76,52 @@ export const getBitbucketHeaders = (bitbucketProvider: Bitbucket) => {
}; };
}; };
export const cloneBitbucketRepository = async ( interface CloneBitbucketRepository {
entity: ApplicationWithBitbucket | ComposeWithBitbucket, appName: string;
logPath: string, bitbucketRepository: string | null;
isCompose = false, bitbucketOwner: string | null;
) => { bitbucketBranch: string | null;
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(); bitbucketId: string | null;
const writeStream = createWriteStream(logPath, { flags: "a" }); enableSubmodules: boolean;
serverId: string | null;
type?: "application" | "compose";
}
export const cloneBitbucketRepository = async ({
type = "application",
...entity
}: CloneBitbucketRepository) => {
let command = "set -e;";
const { const {
appName, appName,
bitbucketRepository, bitbucketRepository,
bitbucketOwner, bitbucketOwner,
bitbucketBranch, bitbucketBranch,
bitbucketId, bitbucketId,
bitbucket,
enableSubmodules, enableSubmodules,
serverId,
} = entity; } = entity;
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(!!serverId);
if (!bitbucketId) { if (!bitbucketId) {
throw new TRPCError({ command += `echo "Error: ❌ Bitbucket Provider not found"; exit 1;`;
code: "NOT_FOUND", return command;
message: "Bitbucket Provider not found",
});
} }
const bitbucket = await findBitbucketById(bitbucketId);
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH; if (!bitbucket) {
command += `echo "Error: ❌ Bitbucket Provider not found"; exit 1;`;
return command;
}
const basePath = type === "compose" ? COMPOSE_PATH : APPLICATIONS_PATH;
const outputPath = join(basePath, appName, "code"); const outputPath = join(basePath, appName, "code");
await recreateDirectory(outputPath); command += `rm -rf ${outputPath};`;
command += `mkdir -p ${outputPath};`;
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`; const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
const cloneUrl = getBitbucketCloneUrl(bitbucket, repoclone); const cloneUrl = getBitbucketCloneUrl(bitbucket, repoclone);
try { command += `echo "Cloning Repo ${repoclone} to ${outputPath}: ✅";`;
writeStream.write(`\nCloning Repo ${repoclone} to ${outputPath}: ✅\n`); command += `git clone --branch ${bitbucketBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} --progress;`;
const cloneArgs = [ return command;
"clone",
"--branch",
bitbucketBranch!,
"--depth",
"1",
...(enableSubmodules ? ["--recurse-submodules"] : []),
cloneUrl,
outputPath,
"--progress",
];
await spawnAsync("git", cloneArgs, (data) => {
if (writeStream.writable) {
writeStream.write(data);
}
});
writeStream.write(`\nCloned ${repoclone} to ${outputPath}: ✅\n`);
} catch (error) {
writeStream.write(`ERROR Cloning: ${error}: ❌`);
throw error;
} finally {
writeStream.end();
}
};
export const cloneRawBitbucketRepository = async (entity: Compose) => {
const { COMPOSE_PATH } = paths();
const {
appName,
bitbucketRepository,
bitbucketOwner,
bitbucketBranch,
bitbucketId,
enableSubmodules,
} = entity;
if (!bitbucketId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Bitbucket Provider not found",
});
}
const bitbucketProvider = await findBitbucketById(bitbucketId);
const basePath = COMPOSE_PATH;
const outputPath = join(basePath, appName, "code");
await recreateDirectory(outputPath);
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
const cloneUrl = getBitbucketCloneUrl(bitbucketProvider, repoclone);
try {
const cloneArgs = [
"clone",
"--branch",
bitbucketBranch!,
"--depth",
"1",
...(enableSubmodules ? ["--recurse-submodules"] : []),
cloneUrl,
outputPath,
"--progress",
];
await spawnAsync("git", cloneArgs);
} catch (error) {
throw error;
}
};
export const cloneRawBitbucketRepositoryRemote = async (compose: Compose) => {
const { COMPOSE_PATH } = paths(true);
const {
appName,
bitbucketRepository,
bitbucketOwner,
bitbucketBranch,
bitbucketId,
serverId,
enableSubmodules,
} = compose;
if (!serverId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Server not found",
});
}
if (!bitbucketId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Bitbucket Provider not found",
});
}
const bitbucketProvider = await findBitbucketById(bitbucketId);
const basePath = COMPOSE_PATH;
const outputPath = join(basePath, appName, "code");
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
const cloneUrl = getBitbucketCloneUrl(bitbucketProvider, repoclone);
try {
const cloneCommand = `
rm -rf ${outputPath};
git clone --branch ${bitbucketBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath}
`;
await execAsyncRemote(serverId, cloneCommand);
} catch (error) {
throw error;
}
};
export const getBitbucketCloneCommand = async (
entity: ApplicationWithBitbucket | ComposeWithBitbucket,
logPath: string,
isCompose = false,
) => {
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(true);
const {
appName,
bitbucketRepository,
bitbucketOwner,
bitbucketBranch,
bitbucketId,
serverId,
enableSubmodules,
} = entity;
if (!serverId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Server not found",
});
}
if (!bitbucketId) {
const command = `
echo "Error: ❌ Bitbucket Provider not found" >> ${logPath};
exit 1;
`;
await execAsyncRemote(serverId, command);
throw new TRPCError({
code: "NOT_FOUND",
message: "Bitbucket Provider not found",
});
}
const bitbucketProvider = await findBitbucketById(bitbucketId);
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
const outputPath = join(basePath, appName, "code");
await recreateDirectory(outputPath);
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
const cloneUrl = getBitbucketCloneUrl(bitbucketProvider, repoclone);
const cloneCommand = `
rm -rf ${outputPath};
mkdir -p ${outputPath};
if ! git clone --branch ${bitbucketBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
echo "❌ [ERROR] Fail to clone the repository ${repoclone}" >> ${logPath};
exit 1;
fi
echo "Cloned ${repoclone} to ${outputPath}: ✅" >> ${logPath};
`;
return cloneCommand;
}; };
export const getBitbucketRepositories = async (bitbucketId?: string) => { export const getBitbucketRepositories = async (bitbucketId?: string) => {
+8 -62
View File
@@ -1,60 +1,6 @@
import { createWriteStream } from "node:fs"; import type { ApplicationNested } from "../builders";
import { type ApplicationNested, mechanizeDockerContainer } from "../builders";
import { pullImage } from "../docker/utils";
interface RegistryAuth { export const buildRemoteDocker = async (application: ApplicationNested) => {
username: string;
password: string;
registryUrl: string;
}
export const buildDocker = async (
application: ApplicationNested,
logPath: string,
): Promise<void> => {
const { buildType, dockerImage, username, password } = application;
const authConfig: Partial<RegistryAuth> = {
username: username || "",
password: password || "",
registryUrl: application.registryUrl || "",
};
const writeStream = createWriteStream(logPath, { flags: "a" });
writeStream.write(`\nBuild ${buildType}\n`);
writeStream.write(`Pulling ${dockerImage}: ✅\n`);
try {
if (!dockerImage) {
throw new Error("Docker image not found");
}
await pullImage(
dockerImage,
(data) => {
if (writeStream.writable) {
writeStream.write(`${data}\n`);
}
},
authConfig,
);
await mechanizeDockerContainer(application);
writeStream.write("\nDocker Deployed: ✅\n");
} catch (error) {
writeStream.write(
`❌ Error: ${error instanceof Error ? error.message : String(error)}`,
);
throw error;
} finally {
writeStream.end();
}
};
export const buildRemoteDocker = async (
application: ApplicationNested,
logPath: string,
) => {
const { registryUrl, dockerImage, username, password } = application; const { registryUrl, dockerImage, username, password } = application;
try { try {
@@ -62,25 +8,25 @@ export const buildRemoteDocker = async (
throw new Error("Docker image not found"); throw new Error("Docker image not found");
} }
let command = ` let command = `
echo "Pulling ${dockerImage}" >> ${logPath}; echo "Pulling ${dockerImage}";
`; `;
if (username && password) { if (username && password) {
command += ` command += `
if ! echo "${password}" | docker login --username "${username}" --password-stdin "${registryUrl || ""}" >> ${logPath} 2>&1; then if ! echo "${password}" | docker login --username "${username}" --password-stdin "${registryUrl || ""}" 2>&1; then
echo "❌ Login failed" >> ${logPath}; echo "❌ Login failed";
exit 1; exit 1;
fi fi
`; `;
} }
command += ` command += `
docker pull ${dockerImage} >> ${logPath} 2>> ${logPath} || { docker pull ${dockerImage} 2>&1 || {
echo "❌ Pulling image failed" >> ${logPath}; echo "❌ Pulling image failed";
exit 1; exit 1;
} }
echo "✅ Pulling image completed." >> ${logPath}; echo "✅ Pulling image completed.";
`; `;
return command; return command;
} catch (error) { } catch (error) {
+87 -324
View File
@@ -1,159 +1,65 @@
import { createWriteStream } from "node:fs";
import path, { join } from "node:path"; import path, { join } from "node:path";
import { paths } from "@dokploy/server/constants"; import { paths } from "@dokploy/server/constants";
import type { Compose } from "@dokploy/server/services/compose";
import { import {
findSSHKeyById, findSSHKeyById,
updateSSHKeyById, updateSSHKeyById,
} from "@dokploy/server/services/ssh-key"; } from "@dokploy/server/services/ssh-key";
import { TRPCError } from "@trpc/server";
import { recreateDirectory } from "../filesystem/directory";
import { execAsync, execAsyncRemote } from "../process/execAsync"; import { execAsync, execAsyncRemote } from "../process/execAsync";
import { spawnAsync } from "../process/spawnAsync";
export const cloneGitRepository = async ( interface CloneGitRepository {
entity: { appName: string;
appName: string; customGitUrl?: string | null;
customGitUrl?: string | null; customGitBranch?: string | null;
customGitBranch?: string | null; customGitSSHKeyId?: string | null;
customGitSSHKeyId?: string | null; enableSubmodules?: boolean;
enableSubmodules?: boolean; serverId: string | null;
}, type?: "application" | "compose";
logPath: string, }
isCompose = false,
) => { export const cloneGitRepository = async ({
const { SSH_PATH, COMPOSE_PATH, APPLICATIONS_PATH } = paths(); type = "application",
...entity
}: CloneGitRepository) => {
let command = "set -e;";
const { const {
appName, appName,
customGitUrl, customGitUrl,
customGitBranch, customGitBranch,
customGitSSHKeyId, customGitSSHKeyId,
enableSubmodules, enableSubmodules,
serverId,
} = entity; } = entity;
const { SSH_PATH, COMPOSE_PATH, APPLICATIONS_PATH } = paths(!!serverId);
if (!customGitUrl || !customGitBranch) { if (!customGitUrl || !customGitBranch) {
throw new TRPCError({ command += `echo "Error: ❌ Repository not found"; exit 1;`;
code: "BAD_REQUEST", return command;
message: "Error: Repository not found",
});
} }
const writeStream = createWriteStream(logPath, { flags: "a" });
const temporalKeyPath = path.join("/tmp", "id_rsa"); const temporalKeyPath = path.join("/tmp", "id_rsa");
if (customGitSSHKeyId) { if (customGitSSHKeyId) {
const sshKey = await findSSHKeyById(customGitSSHKeyId); const sshKey = await findSSHKeyById(customGitSSHKeyId);
await execAsync(` command += `
echo "${sshKey.privateKey}" > ${temporalKeyPath} echo "${sshKey.privateKey}" > ${temporalKeyPath}
chmod 600 ${temporalKeyPath} chmod 600 ${temporalKeyPath};
`); `;
} }
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH; const basePath = type === "compose" ? COMPOSE_PATH : APPLICATIONS_PATH;
const outputPath = join(basePath, appName, "code"); const outputPath = join(basePath, appName, "code");
const knownHostsPath = path.join(SSH_PATH, "known_hosts"); const knownHostsPath = path.join(SSH_PATH, "known_hosts");
try { if (!isHttpOrHttps(customGitUrl)) {
if (!isHttpOrHttps(customGitUrl)) { if (!customGitSSHKeyId) {
if (!customGitSSHKeyId) { command += `echo "Error: ❌ You are trying to clone a ssh repository without a ssh key, please set a ssh key"; exit 1;`;
throw new TRPCError({ return command;
code: "BAD_REQUEST",
message:
"Error: you are trying to clone a ssh repository without a ssh key, please set a ssh key",
});
}
await addHostToKnownHosts(customGitUrl);
} }
await recreateDirectory(outputPath); command += addHostToKnownHostsCommand(customGitUrl);
writeStream.write(
`\nCloning Repo Custom ${customGitUrl} to ${outputPath}: ✅\n`,
);
if (customGitSSHKeyId) {
await updateSSHKeyById({
sshKeyId: customGitSSHKeyId,
lastUsedAt: new Date().toISOString(),
});
}
const { port } = sanitizeRepoPathSSH(customGitUrl);
const cloneArgs = [
"clone",
"--branch",
customGitBranch,
"--depth",
"1",
...(enableSubmodules ? ["--recurse-submodules"] : []),
customGitUrl,
outputPath,
"--progress",
];
await spawnAsync(
"git",
cloneArgs,
(data) => {
if (writeStream.writable) {
writeStream.write(data);
}
},
{
env: {
...process.env,
...(customGitSSHKeyId && {
GIT_SSH_COMMAND: `ssh -i ${temporalKeyPath}${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`,
}),
},
},
);
writeStream.write(`\nCloned Custom Git ${customGitUrl}: ✅\n`);
} catch (error) {
writeStream.write(`\nERROR Cloning Custom Git: ${error}: ❌\n`);
throw error;
} finally {
writeStream.end();
} }
}; command += `rm -rf ${outputPath};`;
command += `mkdir -p ${outputPath};`;
export const getCustomGitCloneCommand = async ( command += `echo "Cloning Repo Custom ${customGitUrl} to ${outputPath}: ✅";`;
entity: {
appName: string;
customGitUrl?: string | null;
customGitBranch?: string | null;
customGitSSHKeyId?: string | null;
serverId: string | null;
enableSubmodules: boolean;
},
logPath: string,
isCompose = false,
) => {
const { SSH_PATH, COMPOSE_PATH, APPLICATIONS_PATH } = paths(true);
const {
appName,
customGitUrl,
customGitBranch,
customGitSSHKeyId,
serverId,
enableSubmodules,
} = entity;
if (!customGitUrl || !customGitBranch) {
const command = `
echo "Error: ❌ Repository not found" >> ${logPath};
exit 1;
`;
await execAsyncRemote(serverId, command);
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error: Repository not found",
});
}
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
const outputPath = join(basePath, appName, "code");
const knownHostsPath = path.join(SSH_PATH, "known_hosts");
if (customGitSSHKeyId) { if (customGitSSHKeyId) {
await updateSSHKeyById({ await updateSSHKeyById({
@@ -161,48 +67,22 @@ export const getCustomGitCloneCommand = async (
lastUsedAt: new Date().toISOString(), lastUsedAt: new Date().toISOString(),
}); });
} }
try {
const command = [];
if (!isHttpOrHttps(customGitUrl)) {
if (!customGitSSHKeyId) {
command.push(
`echo "Error: you are trying to clone a ssh repository without a ssh key, please set a ssh key ❌" >> ${logPath};
exit 1;
`,
);
}
command.push(addHostToKnownHostsCommand(customGitUrl));
}
command.push(`rm -rf ${outputPath};`);
command.push(`mkdir -p ${outputPath};`);
command.push(
`echo "Cloning Custom Git ${customGitUrl}" to ${outputPath}: ✅ >> ${logPath};`,
);
if (customGitSSHKeyId) {
const sshKey = await findSSHKeyById(customGitSSHKeyId);
const { port } = sanitizeRepoPathSSH(customGitUrl);
const gitSshCommand = `ssh -i /tmp/id_rsa${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`;
command.push(
`
echo "${sshKey.privateKey}" > /tmp/id_rsa
chmod 600 /tmp/id_rsa
export GIT_SSH_COMMAND="${gitSshCommand}"
`,
);
}
command.push( if (customGitSSHKeyId) {
`if ! git clone --branch ${customGitBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${customGitUrl} ${outputPath} >> ${logPath} 2>&1; then const sshKey = await findSSHKeyById(customGitSSHKeyId);
echo "❌ [ERROR] Fail to clone the repository ${customGitUrl}" >> ${logPath}; const { port } = sanitizeRepoPathSSH(customGitUrl);
const gitSshCommand = `ssh -i /tmp/id_rsa${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`;
command += `echo "${sshKey.privateKey}" > /tmp/id_rsa;`;
command += "chmod 600 /tmp/id_rsa;";
command += `export GIT_SSH_COMMAND="${gitSshCommand}";`;
}
command += `if ! git clone --branch ${customGitBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${customGitUrl} ${outputPath}; then
echo "❌ [ERROR] Fail to clone the repository ${customGitUrl}";
exit 1; exit 1;
fi fi
`, `;
);
command.push(`echo "Cloned Custom Git ${customGitUrl}: ✅" >> ${logPath};`); return command;
return command.join("\n");
} catch (error) {
throw error;
}
}; };
const isHttpOrHttps = (url: string): boolean => { const isHttpOrHttps = (url: string): boolean => {
@@ -210,19 +90,19 @@ const isHttpOrHttps = (url: string): boolean => {
return regex.test(url); return regex.test(url);
}; };
const addHostToKnownHosts = async (repositoryURL: string) => { // const addHostToKnownHosts = async (repositoryURL: string) => {
const { SSH_PATH } = paths(); // const { SSH_PATH } = paths();
const { domain, port } = sanitizeRepoPathSSH(repositoryURL); // const { domain, port } = sanitizeRepoPathSSH(repositoryURL);
const knownHostsPath = path.join(SSH_PATH, "known_hosts"); // const knownHostsPath = path.join(SSH_PATH, "known_hosts");
const command = `ssh-keyscan -p ${port} ${domain} >> ${knownHostsPath}`; // const command = `ssh-keyscan -p ${port} ${domain} >> ${knownHostsPath}`;
try { // try {
await execAsync(command); // await execAsync(command);
} catch (error) { // } catch (error) {
console.error(`Error adding host to known_hosts: ${error}`); // console.error(`Error adding host to known_hosts: ${error}`);
throw error; // throw error;
} // }
}; // };
const addHostToKnownHostsCommand = (repositoryURL: string) => { const addHostToKnownHostsCommand = (repositoryURL: string) => {
const { SSH_PATH } = paths(true); const { SSH_PATH } = paths(true);
@@ -267,160 +147,43 @@ const sanitizeRepoPathSSH = (input: string) => {
}; };
}; };
export const cloneGitRawRepository = async (entity: { interface Props {
appName: string; appName: string;
customGitUrl?: string | null; type?: "application" | "compose";
customGitBranch?: string | null; serverId: string | null;
customGitSSHKeyId?: string | null; }
enableSubmodules?: boolean;
}) => {
const {
appName,
customGitUrl,
customGitBranch,
customGitSSHKeyId,
enableSubmodules,
} = entity;
if (!customGitUrl || !customGitBranch) { export const getGitCommitInfo = async ({
throw new TRPCError({ appName,
code: "BAD_REQUEST", type = "application",
message: "Error: Repository not found", serverId,
}); }: Props) => {
} const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(!!serverId);
const basePath = type === "compose" ? COMPOSE_PATH : APPLICATIONS_PATH;
const { SSH_PATH, COMPOSE_PATH } = paths();
const temporalKeyPath = path.join("/tmp", "id_rsa");
const basePath = COMPOSE_PATH;
const outputPath = join(basePath, appName, "code"); const outputPath = join(basePath, appName, "code");
const knownHostsPath = path.join(SSH_PATH, "known_hosts"); let stdoutResult = "";
const result = {
if (customGitSSHKeyId) { message: "",
const sshKey = await findSSHKeyById(customGitSSHKeyId); hash: "",
};
await execAsync(`
echo "${sshKey.privateKey}" > ${temporalKeyPath}
chmod 600 ${temporalKeyPath}
`);
}
try { try {
if (!isHttpOrHttps(customGitUrl)) { const gitCommand = `git -C ${outputPath} log -1 --pretty=format:"%H---DELIMITER---%B"`;
if (!customGitSSHKeyId) { if (serverId) {
throw new TRPCError({ const { stdout } = await execAsyncRemote(serverId, gitCommand);
code: "BAD_REQUEST", stdoutResult = stdout.trim();
message: } else {
"Error: you are trying to clone a ssh repository without a ssh key, please set a ssh key", const { stdout } = await execAsync(gitCommand);
}); stdoutResult = stdout.trim();
}
await addHostToKnownHosts(customGitUrl);
}
await recreateDirectory(outputPath);
if (customGitSSHKeyId) {
await updateSSHKeyById({
sshKeyId: customGitSSHKeyId,
lastUsedAt: new Date().toISOString(),
});
} }
const { port } = sanitizeRepoPathSSH(customGitUrl); const parts = stdoutResult.split("---DELIMITER---");
const cloneArgs = [ if (parts && parts.length === 2) {
"clone", result.hash = parts[0]?.trim() || "";
"--branch", result.message = parts[1]?.trim() || "";
customGitBranch, }
"--depth",
"1",
...(enableSubmodules ? ["--recurse-submodules"] : []),
customGitUrl,
outputPath,
"--progress",
];
await spawnAsync("git", cloneArgs, (_data) => {}, {
env: {
...process.env,
...(customGitSSHKeyId && {
GIT_SSH_COMMAND: `ssh -i ${temporalKeyPath}${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`,
}),
},
});
} catch (error) { } catch (error) {
throw error; console.error(`Error getting git commit info: ${error}`);
} return null;
};
export const cloneRawGitRepositoryRemote = async (compose: Compose) => {
const {
appName,
customGitBranch,
customGitUrl,
customGitSSHKeyId,
serverId,
enableSubmodules,
} = compose;
if (!serverId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Server not found",
});
}
if (!customGitUrl) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Git Provider not found",
});
}
const { SSH_PATH, COMPOSE_PATH } = paths(true);
const basePath = COMPOSE_PATH;
const outputPath = join(basePath, appName, "code");
const knownHostsPath = path.join(SSH_PATH, "known_hosts");
if (customGitSSHKeyId) {
await updateSSHKeyById({
sshKeyId: customGitSSHKeyId,
lastUsedAt: new Date().toISOString(),
});
}
try {
const command = [];
if (!isHttpOrHttps(customGitUrl)) {
if (!customGitSSHKeyId) {
command.push(
`echo "Error: you are trying to clone a ssh repository without a ssh key, please set a ssh key ❌" ;
exit 1;
`,
);
}
command.push(addHostToKnownHostsCommand(customGitUrl));
}
command.push(`rm -rf ${outputPath};`);
command.push(`mkdir -p ${outputPath};`);
if (customGitSSHKeyId) {
const sshKey = await findSSHKeyById(customGitSSHKeyId);
const { port } = sanitizeRepoPathSSH(customGitUrl);
const gitSshCommand = `ssh -i /tmp/id_rsa${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`;
command.push(
`
echo "${sshKey.privateKey}" > /tmp/id_rsa
chmod 600 /tmp/id_rsa
export GIT_SSH_COMMAND="${gitSshCommand}"
`,
);
}
command.push(
`if ! git clone --branch ${customGitBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${customGitUrl} ${outputPath} ; then
echo "[ERROR] Fail to clone the repository ";
exit 1;
fi
`,
);
await execAsyncRemote(serverId, command.join("\n"));
} catch (error) {
throw error;
} }
return result;
}; };
+31 -213
View File
@@ -1,7 +1,5 @@
import { createWriteStream } from "node:fs";
import { join } from "node:path"; import { join } from "node:path";
import { paths } from "@dokploy/server/constants"; import { paths } from "@dokploy/server/constants";
import type { Compose } from "@dokploy/server/services/compose";
import { import {
findGiteaById, findGiteaById,
type Gitea, type Gitea,
@@ -9,9 +7,6 @@ import {
} from "@dokploy/server/services/gitea"; } from "@dokploy/server/services/gitea";
import type { InferResultType } from "@dokploy/server/types/with"; import type { InferResultType } from "@dokploy/server/types/with";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { recreateDirectory } from "../filesystem/directory";
import { execAsyncRemote } from "../process/execAsync";
import { spawnAsync } from "../process/spawnAsync";
export const getErrorCloneRequirements = (entity: { export const getErrorCloneRequirements = (entity: {
giteaRepository?: string | null; giteaRepository?: string | null;
@@ -119,79 +114,27 @@ export type ApplicationWithGitea = InferResultType<
export type ComposeWithGitea = InferResultType<"compose", { gitea: true }>; export type ComposeWithGitea = InferResultType<"compose", { gitea: true }>;
export const getGiteaCloneCommand = async ( type GiteaClone = (ApplicationWithGitea | ComposeWithGitea) & {
entity: ApplicationWithGitea | ComposeWithGitea, serverId: string | null;
logPath: string, type?: "application" | "compose";
isCompose = false,
) => {
const {
appName,
giteaBranch,
giteaId,
giteaOwner,
giteaRepository,
serverId,
enableSubmodules,
} = entity;
if (!serverId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Server not found",
});
}
if (!giteaId) {
const command = `
echo "Error: ❌ Gitlab Provider not found" >> ${logPath};
exit 1;
`;
await execAsyncRemote(serverId, command);
throw new TRPCError({
code: "NOT_FOUND",
message: "Gitea Provider not found",
});
}
// Use paths(true) for remote operations
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(true);
await refreshGiteaToken(giteaId);
const gitea = await findGiteaById(giteaId);
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
const outputPath = join(basePath, appName, "code");
const repoClone = `${giteaOwner}/${giteaRepository}.git`;
const cloneUrl = buildGiteaCloneUrl(
gitea?.giteaUrl!,
gitea?.accessToken!,
giteaOwner!,
giteaRepository!,
);
const cloneCommand = `
rm -rf ${outputPath};
mkdir -p ${outputPath};
if ! git clone --branch ${giteaBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
echo "❌ [ERROR] Failed to clone the repository ${repoClone}" >> ${logPath};
exit 1;
fi
echo "Cloned ${repoClone} to ${outputPath}: ✅" >> ${logPath};
`;
return cloneCommand;
}; };
export const cloneGiteaRepository = async ( interface CloneGiteaRepository {
entity: ApplicationWithGitea | ComposeWithGitea, appName: string;
logPath: string, giteaBranch: string | null;
isCompose = false, giteaId: string | null;
) => { giteaOwner: string | null;
const { APPLICATIONS_PATH, COMPOSE_PATH } = paths(); giteaRepository: string | null;
enableSubmodules: boolean;
serverId: string | null;
type?: "application" | "compose";
}
const writeStream = createWriteStream(logPath, { flags: "a" }); export const cloneGiteaRepository = async ({
type = "application",
...entity
}: CloneGiteaRepository) => {
let command = "set -e;";
const { const {
appName, appName,
giteaBranch, giteaBranch,
@@ -199,27 +142,27 @@ export const cloneGiteaRepository = async (
giteaOwner, giteaOwner,
giteaRepository, giteaRepository,
enableSubmodules, enableSubmodules,
serverId,
} = entity; } = entity;
const { APPLICATIONS_PATH, COMPOSE_PATH } = paths(!!serverId);
if (!giteaId) { if (!giteaId) {
throw new TRPCError({ command += `echo "Error: ❌ Gitea Provider not found"; exit 1;`;
code: "NOT_FOUND", return command;
message: "Gitea Provider not found",
});
} }
await refreshGiteaToken(giteaId); await refreshGiteaToken(giteaId);
const giteaProvider = await findGiteaById(giteaId); const giteaProvider = await findGiteaById(giteaId);
if (!giteaProvider) { if (!giteaProvider) {
throw new TRPCError({ command += `echo "❌ [ERROR] Gitea provider not found in the database"; exit 1;`;
code: "NOT_FOUND", return command;
message: "Gitea provider not found in the database",
});
} }
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH; const basePath = type === "compose" ? COMPOSE_PATH : APPLICATIONS_PATH;
const outputPath = join(basePath, appName, "code"); const outputPath = join(basePath, appName, "code");
await recreateDirectory(outputPath); command += `rm -rf ${outputPath};`;
command += `mkdir -p ${outputPath};`;
const repoClone = `${giteaOwner}/${giteaRepository}.git`; const repoClone = `${giteaOwner}/${giteaRepository}.git`;
const cloneUrl = buildGiteaCloneUrl( const cloneUrl = buildGiteaCloneUrl(
@@ -229,134 +172,9 @@ export const cloneGiteaRepository = async (
giteaRepository!, giteaRepository!,
); );
writeStream.write(`\nCloning Repo ${repoClone} to ${outputPath}...\n`); command += `echo "Cloning Repo ${repoClone} to ${outputPath}: ✅";`;
command += `git clone --branch ${giteaBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} --progress;`;
try { return command;
await spawnAsync(
"git",
[
"clone",
"--branch",
giteaBranch!,
"--depth",
"1",
...(enableSubmodules ? ["--recurse-submodules"] : []),
cloneUrl,
outputPath,
"--progress",
],
(data) => {
if (writeStream.writable) {
writeStream.write(data);
}
},
);
writeStream.write(`\nCloned ${repoClone}: ✅\n`);
} catch (error) {
writeStream.write(`ERROR Cloning: ${error}: ❌`);
throw error;
} finally {
writeStream.end();
}
};
export const cloneRawGiteaRepository = async (entity: Compose) => {
const {
appName,
giteaRepository,
giteaOwner,
giteaBranch,
giteaId,
enableSubmodules,
} = entity;
const { COMPOSE_PATH } = paths();
if (!giteaId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Gitea Provider not found",
});
}
await refreshGiteaToken(giteaId);
const giteaProvider = await findGiteaById(giteaId);
if (!giteaProvider) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Gitea provider not found in the database",
});
}
const basePath = COMPOSE_PATH;
const outputPath = join(basePath, appName, "code");
await recreateDirectory(outputPath);
const cloneUrl = buildGiteaCloneUrl(
giteaProvider.giteaUrl,
giteaProvider.accessToken!,
giteaOwner!,
giteaRepository!,
);
try {
await spawnAsync("git", [
"clone",
"--branch",
giteaBranch!,
"--depth",
"1",
...(enableSubmodules ? ["--recurse-submodules"] : []),
cloneUrl,
outputPath,
"--progress",
]);
} catch (error) {
throw error;
}
};
export const cloneRawGiteaRepositoryRemote = async (compose: Compose) => {
const {
appName,
giteaRepository,
giteaOwner,
giteaBranch,
giteaId,
serverId,
enableSubmodules,
} = compose;
if (!serverId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Server not found",
});
}
if (!giteaId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Gitea Provider not found",
});
}
const { COMPOSE_PATH } = paths(true);
const giteaProvider = await findGiteaById(giteaId);
const basePath = COMPOSE_PATH;
const outputPath = join(basePath, appName, "code");
const cloneUrl = buildGiteaCloneUrl(
giteaProvider.giteaUrl,
giteaProvider.accessToken!,
giteaOwner!,
giteaRepository!,
);
try {
const command = `
rm -rf ${outputPath};
git clone --branch ${giteaBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath}
`;
await execAsyncRemote(serverId, command);
} catch (error) {
throw error;
}
}; };
export const haveGiteaRequirements = (giteaProvider: Gitea) => { export const haveGiteaRequirements = (giteaProvider: Gitea) => {
+23 -209
View File
@@ -1,16 +1,11 @@
import { createWriteStream } from "node:fs";
import { join } from "node:path"; import { join } from "node:path";
import { paths } from "@dokploy/server/constants"; import { paths } from "@dokploy/server/constants";
import type { apiFindGithubBranches } from "@dokploy/server/db/schema"; import type { apiFindGithubBranches } from "@dokploy/server/db/schema";
import type { Compose } from "@dokploy/server/services/compose";
import { findGithubById, type Github } from "@dokploy/server/services/github"; import { findGithubById, type Github } from "@dokploy/server/services/github";
import type { InferResultType } from "@dokploy/server/types/with"; import type { InferResultType } from "@dokploy/server/types/with";
import { createAppAuth } from "@octokit/auth-app"; import { createAppAuth } from "@octokit/auth-app";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { Octokit } from "octokit"; import { Octokit } from "octokit";
import { recreateDirectory } from "../filesystem/directory";
import { execAsyncRemote } from "../process/execAsync";
import { spawnAsync } from "../process/spawnAsync";
export const authGithub = (githubProvider: Github): Octokit => { export const authGithub = (githubProvider: Github): Octokit => {
if (!haveGithubRequirements(githubProvider)) { if (!haveGithubRequirements(githubProvider)) {
@@ -123,42 +118,39 @@ interface CloneGithubRepository {
branch: string | null; branch: string | null;
githubId: string | null; githubId: string | null;
repository: string | null; repository: string | null;
logPath: string;
type?: "application" | "compose"; type?: "application" | "compose";
enableSubmodules: boolean; enableSubmodules: boolean;
serverId: string | null;
} }
export const cloneGithubRepository = async ({ export const cloneGithubRepository = async ({
logPath,
type = "application", type = "application",
...entity ...entity
}: CloneGithubRepository) => { }: CloneGithubRepository) => {
let command = "set -e;";
const isCompose = type === "compose"; const isCompose = type === "compose";
const { APPLICATIONS_PATH, COMPOSE_PATH } = paths(); const {
const writeStream = createWriteStream(logPath, { flags: "a" }); appName,
const { appName, repository, owner, branch, githubId, enableSubmodules } = repository,
entity; owner,
branch,
githubId,
enableSubmodules,
serverId,
} = entity;
const { APPLICATIONS_PATH, COMPOSE_PATH } = paths(!!serverId);
if (!githubId) { if (!githubId) {
throw new TRPCError({ command += `echo "Error: ❌ Github Provider not found"; exit 1;`;
code: "NOT_FOUND",
message: "GitHub Provider not found", return command;
});
} }
const requirements = getErrorCloneRequirements(entity); const requirements = getErrorCloneRequirements(entity);
// Check if requirements are met // Check if requirements are met
if (requirements.length > 0) { if (requirements.length > 0) {
writeStream.write( command += `echo "GitHub Repository configuration failed for application: ${appName}"; echo "Reasons:"; echo "${requirements.join("\n")}"; exit 1;`;
`\nGitHub Repository configuration failed for application: ${appName}\n`, return command;
);
writeStream.write("Reasons:\n");
writeStream.write(requirements.join("\n"));
writeStream.end();
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error: GitHub repository information is incomplete.",
});
} }
const githubProvider = await findGithubById(githubId); const githubProvider = await findGithubById(githubId);
@@ -167,193 +159,15 @@ export const cloneGithubRepository = async ({
const octokit = authGithub(githubProvider); const octokit = authGithub(githubProvider);
const token = await getGithubToken(octokit); const token = await getGithubToken(octokit);
const repoclone = `github.com/${owner}/${repository}.git`; const repoclone = `github.com/${owner}/${repository}.git`;
await recreateDirectory(outputPath); // await recreateDirectory(outputPath);
command += `rm -rf ${outputPath};`;
command += `mkdir -p ${outputPath};`;
const cloneUrl = `https://oauth2:${token}@${repoclone}`; const cloneUrl = `https://oauth2:${token}@${repoclone}`;
try { command += `echo "Cloning Repo ${repoclone} to ${outputPath}: ✅";`;
writeStream.write(`\nCloning Repo ${repoclone} to ${outputPath}: ✅\n`); command += `git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} --progress;`;
const cloneArgs = [
"clone",
"--branch",
branch!,
"--depth",
"1",
...(enableSubmodules ? ["--recurse-submodules"] : []),
cloneUrl,
outputPath,
"--progress",
];
await spawnAsync("git", cloneArgs, (data) => { return command;
if (writeStream.writable) {
writeStream.write(data);
}
});
writeStream.write(`\nCloned ${repoclone}: ✅\n`);
} catch (error) {
writeStream.write(`ERROR Cloning: ${error}: ❌`);
throw error;
} finally {
writeStream.end();
}
};
export const getGithubCloneCommand = async ({
logPath,
type = "application",
...entity
}: CloneGithubRepository & { serverId: string }) => {
const {
appName,
repository,
owner,
branch,
githubId,
serverId,
enableSubmodules,
} = entity;
const isCompose = type === "compose";
if (!serverId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Server not found",
});
}
if (!githubId) {
const command = `
echo "Error: ❌ Github Provider not found" >> ${logPath};
exit 1;
`;
await execAsyncRemote(serverId, command);
throw new TRPCError({
code: "NOT_FOUND",
message: "GitHub Provider not found",
});
}
const requirements = getErrorCloneRequirements(entity);
// Build log messages
let logMessages = "";
if (requirements.length > 0) {
logMessages += `\nGitHub Repository configuration failed for application: ${appName}\n`;
logMessages += "Reasons:\n";
logMessages += requirements.join("\n");
const escapedLogMessages = logMessages
.replace(/\\/g, "\\\\")
.replace(/"/g, '\\"')
.replace(/\n/g, "\\n");
const bashCommand = `
echo "${escapedLogMessages}" >> ${logPath};
exit 1; # Exit with error code
`;
await execAsyncRemote(serverId, bashCommand);
return;
}
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(true);
const githubProvider = await findGithubById(githubId);
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
const outputPath = join(basePath, appName, "code");
const octokit = authGithub(githubProvider);
const token = await getGithubToken(octokit);
const repoclone = `github.com/${owner}/${repository}.git`;
const cloneUrl = `https://oauth2:${token}@${repoclone}`;
const cloneCommand = `
rm -rf ${outputPath};
mkdir -p ${outputPath};
if ! git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
echo "❌ [ERROR] Fail to clone repository ${repoclone}" >> ${logPath};
exit 1;
fi
echo "Cloned ${repoclone} to ${outputPath}: ✅" >> ${logPath};
`;
return cloneCommand;
};
export const cloneRawGithubRepository = async (entity: Compose) => {
const { appName, repository, owner, branch, githubId, enableSubmodules } =
entity;
if (!githubId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "GitHub Provider not found",
});
}
const { COMPOSE_PATH } = paths();
const githubProvider = await findGithubById(githubId);
const basePath = COMPOSE_PATH;
const outputPath = join(basePath, appName, "code");
const octokit = authGithub(githubProvider);
const token = await getGithubToken(octokit);
const repoclone = `github.com/${owner}/${repository}.git`;
await recreateDirectory(outputPath);
const cloneUrl = `https://oauth2:${token}@${repoclone}`;
try {
const cloneArgs = [
"clone",
"--branch",
branch!,
"--depth",
"1",
...(enableSubmodules ? ["--recurse-submodules"] : []),
cloneUrl,
outputPath,
"--progress",
];
await spawnAsync("git", cloneArgs);
} catch (error) {
throw error;
}
};
export const cloneRawGithubRepositoryRemote = async (compose: Compose) => {
const {
appName,
repository,
owner,
branch,
githubId,
serverId,
enableSubmodules,
} = compose;
if (!serverId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Server not found",
});
}
if (!githubId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "GitHub Provider not found",
});
}
const { COMPOSE_PATH } = paths(true);
const githubProvider = await findGithubById(githubId);
const basePath = COMPOSE_PATH;
const outputPath = join(basePath, appName, "code");
const octokit = authGithub(githubProvider);
const token = await getGithubToken(octokit);
const repoclone = `github.com/${owner}/${repository}.git`;
const cloneUrl = `https://oauth2:${token}@${repoclone}`;
try {
const command = `
rm -rf ${outputPath};
git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath}
`;
await execAsyncRemote(serverId, command);
} catch (error) {
throw error;
}
}; };
export const getGithubRepositories = async (githubId?: string) => { export const getGithubRepositories = async (githubId?: string) => {
+27 -213
View File
@@ -1,8 +1,6 @@
import { createWriteStream } from "node:fs";
import { join } from "node:path"; import { join } from "node:path";
import { paths } from "@dokploy/server/constants"; import { paths } from "@dokploy/server/constants";
import type { apiGitlabTestConnection } from "@dokploy/server/db/schema"; import type { apiGitlabTestConnection } from "@dokploy/server/db/schema";
import type { Compose } from "@dokploy/server/services/compose";
import { import {
findGitlabById, findGitlabById,
type Gitlab, type Gitlab,
@@ -10,9 +8,6 @@ import {
} from "@dokploy/server/services/gitlab"; } from "@dokploy/server/services/gitlab";
import type { InferResultType } from "@dokploy/server/types/with"; import type { InferResultType } from "@dokploy/server/types/with";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { recreateDirectory } from "../filesystem/directory";
import { execAsyncRemote } from "../process/execAsync";
import { spawnAsync } from "../process/spawnAsync";
export const refreshGitlabToken = async (gitlabProviderId: string) => { export const refreshGitlabToken = async (gitlabProviderId: string) => {
const gitlabProvider = await findGitlabById(gitlabProviderId); const gitlabProvider = await findGitlabById(gitlabProviderId);
@@ -102,25 +97,34 @@ const getGitlabCloneUrl = (gitlab: GitlabInfo, repoClone: string) => {
return cloneUrl; return cloneUrl;
}; };
export const cloneGitlabRepository = async ( interface CloneGitlabRepository {
entity: ApplicationWithGitlab | ComposeWithGitlab, appName: string;
logPath: string, gitlabBranch: string | null;
isCompose = false, gitlabId: string | null;
) => { gitlabPathNamespace: string | null;
const writeStream = createWriteStream(logPath, { flags: "a" }); enableSubmodules: boolean;
serverId: string | null;
type?: "application" | "compose";
}
export const cloneGitlabRepository = async ({
type = "application",
...entity
}: CloneGitlabRepository) => {
let command = "set -e;";
const { const {
appName, appName,
gitlabBranch, gitlabBranch,
gitlabId, gitlabId,
gitlabPathNamespace, gitlabPathNamespace,
enableSubmodules, enableSubmodules,
serverId,
} = entity; } = entity;
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(!!serverId);
if (!gitlabId) { if (!gitlabId) {
throw new TRPCError({ command += `echo "Error: ❌ Gitlab Provider not found"; exit 1;`;
code: "NOT_FOUND", return command;
message: "Gitlab Provider not found",
});
} }
await refreshGitlabToken(gitlabId); await refreshGitlabToken(gitlabId);
@@ -130,127 +134,19 @@ export const cloneGitlabRepository = async (
// Check if requirements are met // Check if requirements are met
if (requirements.length > 0) { if (requirements.length > 0) {
writeStream.write( command += `echo "❌ [ERROR] GitLab Repository configuration failed for application: ${appName}"; echo "Reasons:"; echo "${requirements.join("\n")}"; exit 1;`;
`\nGitLab Repository configuration failed for application: ${appName}\n`, return command;
);
writeStream.write("Reasons:\n");
writeStream.write(requirements.join("\n"));
writeStream.end();
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error: GitLab repository information is incomplete.",
});
} }
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(); const basePath = type === "compose" ? COMPOSE_PATH : APPLICATIONS_PATH;
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
const outputPath = join(basePath, appName, "code"); const outputPath = join(basePath, appName, "code");
await recreateDirectory(outputPath); command += `rm -rf ${outputPath};`;
command += `mkdir -p ${outputPath};`;
const repoClone = getGitlabRepoClone(gitlab, gitlabPathNamespace); const repoClone = getGitlabRepoClone(gitlab, gitlabPathNamespace);
const cloneUrl = getGitlabCloneUrl(gitlab, repoClone); const cloneUrl = getGitlabCloneUrl(gitlab, repoClone);
try { command += `echo "Cloning Repo ${repoClone} to ${outputPath}: ✅";`;
writeStream.write(`\nCloning Repo ${repoClone} to ${outputPath}: ✅\n`); command += `git clone --branch ${gitlabBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} --progress;`;
const cloneArgs = [ return command;
"clone",
"--branch",
gitlabBranch!,
"--depth",
"1",
...(enableSubmodules ? ["--recurse-submodules"] : []),
cloneUrl,
outputPath,
"--progress",
];
await spawnAsync("git", cloneArgs, (data) => {
if (writeStream.writable) {
writeStream.write(data);
}
});
writeStream.write(`\nCloned ${repoClone}: ✅\n`);
} catch (error) {
writeStream.write(`ERROR Cloning: ${error}: ❌`);
throw error;
} finally {
writeStream.end();
}
};
export const getGitlabCloneCommand = async (
entity: ApplicationWithGitlab | ComposeWithGitlab,
logPath: string,
isCompose = false,
) => {
const {
appName,
gitlabPathNamespace,
gitlabBranch,
gitlabId,
serverId,
enableSubmodules,
} = entity;
if (!serverId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Server not found",
});
}
if (!gitlabId) {
const command = `
echo "Error: ❌ Gitlab Provider not found" >> ${logPath};
exit 1;
`;
await execAsyncRemote(serverId, command);
throw new TRPCError({
code: "NOT_FOUND",
message: "Gitlab Provider not found",
});
}
const requirements = getErrorCloneRequirements(entity);
// Build log messages
let logMessages = "";
if (requirements.length > 0) {
logMessages += `\nGitLab Repository configuration failed for application: ${appName}\n`;
logMessages += "Reasons:\n";
logMessages += requirements.join("\n");
const escapedLogMessages = logMessages
.replace(/\\/g, "\\\\")
.replace(/"/g, '\\"')
.replace(/\n/g, "\\n");
const bashCommand = `
echo "${escapedLogMessages}" >> ${logPath};
exit 1; # Exit with error code
`;
await execAsyncRemote(serverId, bashCommand);
return;
}
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(true);
await refreshGitlabToken(gitlabId);
const gitlab = await findGitlabById(gitlabId);
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
const outputPath = join(basePath, appName, "code");
await recreateDirectory(outputPath);
const repoClone = getGitlabRepoClone(gitlab, gitlabPathNamespace);
const cloneUrl = getGitlabCloneUrl(gitlab, repoClone);
const cloneCommand = `
rm -rf ${outputPath};
mkdir -p ${outputPath};
if ! git clone --branch ${gitlabBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
echo "❌ [ERROR] Fail to clone the repository ${repoClone}" >> ${logPath};
exit 1;
fi
echo "Cloned ${repoClone} to ${outputPath}: ✅" >> ${logPath};
`;
return cloneCommand;
}; };
export const getGitlabRepositories = async (gitlabId?: string) => { export const getGitlabRepositories = async (gitlabId?: string) => {
@@ -355,88 +251,6 @@ export const getGitlabBranches = async (input: {
}[]; }[];
}; };
export const cloneRawGitlabRepository = async (entity: Compose) => {
const {
appName,
gitlabBranch,
gitlabId,
gitlabPathNamespace,
enableSubmodules,
} = entity;
if (!gitlabId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Gitlab Provider not found",
});
}
const { COMPOSE_PATH } = paths();
await refreshGitlabToken(gitlabId);
const gitlabProvider = await findGitlabById(gitlabId);
const basePath = COMPOSE_PATH;
const outputPath = join(basePath, appName, "code");
await recreateDirectory(outputPath);
const repoClone = getGitlabRepoClone(gitlabProvider, gitlabPathNamespace);
const cloneUrl = getGitlabCloneUrl(gitlabProvider, repoClone);
try {
const cloneArgs = [
"clone",
"--branch",
gitlabBranch!,
"--depth",
"1",
...(enableSubmodules ? ["--recurse-submodules"] : []),
cloneUrl,
outputPath,
"--progress",
];
await spawnAsync("git", cloneArgs);
} catch (error) {
throw error;
}
};
export const cloneRawGitlabRepositoryRemote = async (compose: Compose) => {
const {
appName,
gitlabPathNamespace,
gitlabBranch,
gitlabId,
serverId,
enableSubmodules,
} = compose;
if (!serverId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Server not found",
});
}
if (!gitlabId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Gitlab Provider not found",
});
}
const { COMPOSE_PATH } = paths(true);
await refreshGitlabToken(gitlabId);
const gitlabProvider = await findGitlabById(gitlabId);
const basePath = COMPOSE_PATH;
const outputPath = join(basePath, appName, "code");
const repoClone = getGitlabRepoClone(gitlabProvider, gitlabPathNamespace);
const cloneUrl = getGitlabCloneUrl(gitlabProvider, repoClone);
try {
const command = `
rm -rf ${outputPath};
git clone --branch ${gitlabBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath}
`;
await execAsyncRemote(serverId, command);
} catch (error) {
throw error;
}
};
export const testGitlabConnection = async ( export const testGitlabConnection = async (
input: typeof apiGitlabTestConnection._type, input: typeof apiGitlabTestConnection._type,
) => { ) => {
+3 -65
View File
@@ -1,40 +1,10 @@
import { createWriteStream } from "node:fs";
import { writeFile } from "node:fs/promises";
import { join } from "node:path"; import { join } from "node:path";
import { paths } from "@dokploy/server/constants"; import { paths } from "@dokploy/server/constants";
import type { Compose } from "@dokploy/server/services/compose"; import type { Compose } from "@dokploy/server/services/compose";
import { encodeBase64 } from "../docker/utils"; import { encodeBase64 } from "../docker/utils";
import { recreateDirectory } from "../filesystem/directory";
import { execAsyncRemote } from "../process/execAsync";
export const createComposeFile = async (compose: Compose, logPath: string) => { export const getCreateComposeFileCommand = (compose: Compose) => {
const { COMPOSE_PATH } = paths(); const { COMPOSE_PATH } = paths(!!compose.serverId);
const { appName, composeFile } = compose;
const writeStream = createWriteStream(logPath, { flags: "a" });
const outputPath = join(COMPOSE_PATH, appName, "code");
try {
await recreateDirectory(outputPath);
writeStream.write(
`\nCreating File 'docker-compose.yml' to ${outputPath}: ✅\n`,
);
await writeFile(join(outputPath, "docker-compose.yml"), composeFile);
writeStream.write(`\nFile 'docker-compose.yml' created: ✅\n`);
} catch (error) {
writeStream.write(`\nERROR Creating Compose File: ${error}: ❌\n`);
throw error;
} finally {
writeStream.end();
}
};
export const getCreateComposeFileCommand = (
compose: Compose,
logPath: string,
) => {
const { COMPOSE_PATH } = paths(true);
const { appName, composeFile } = compose; const { appName, composeFile } = compose;
const outputPath = join(COMPOSE_PATH, appName, "code"); const outputPath = join(COMPOSE_PATH, appName, "code");
const filePath = join(outputPath, "docker-compose.yml"); const filePath = join(outputPath, "docker-compose.yml");
@@ -43,39 +13,7 @@ export const getCreateComposeFileCommand = (
rm -rf ${outputPath}; rm -rf ${outputPath};
mkdir -p ${outputPath}; mkdir -p ${outputPath};
echo "${encodedContent}" | base64 -d > "${filePath}"; echo "${encodedContent}" | base64 -d > "${filePath}";
echo "File 'docker-compose.yml' created: ✅" >> ${logPath}; echo "File 'docker-compose.yml' created: ✅";
`; `;
return bashCommand; return bashCommand;
}; };
export const createComposeFileRaw = async (compose: Compose) => {
const { COMPOSE_PATH } = paths();
const { appName, composeFile } = compose;
const outputPath = join(COMPOSE_PATH, appName, "code");
const filePath = join(outputPath, "docker-compose.yml");
try {
await recreateDirectory(outputPath);
await writeFile(filePath, composeFile);
} catch (error) {
throw error;
}
};
export const createComposeFileRawRemote = async (compose: Compose) => {
const { COMPOSE_PATH } = paths(true);
const { appName, composeFile, serverId } = compose;
const outputPath = join(COMPOSE_PATH, appName, "code");
const filePath = join(outputPath, "docker-compose.yml");
try {
const encodedContent = encodeBase64(composeFile);
const command = `
rm -rf ${outputPath};
mkdir -p ${outputPath};
echo "${encodedContent}" | base64 -d > "${filePath}";
`;
await execAsyncRemote(serverId, command);
} catch (error) {
throw error;
}
};
+4 -4
View File
@@ -7,7 +7,7 @@ export const getPostgresRestoreCommand = (
database: string, database: string,
databaseUser: string, databaseUser: string,
) => { ) => {
return `docker exec -i $CONTAINER_ID sh -c "pg_restore -U ${databaseUser} -d ${database} -O --clean --if-exists"`; return `docker exec -i $CONTAINER_ID sh -c "pg_restore -U '${databaseUser}' -d ${database} -O --clean --if-exists"`;
}; };
export const getMariadbRestoreCommand = ( export const getMariadbRestoreCommand = (
@@ -15,14 +15,14 @@ export const getMariadbRestoreCommand = (
databaseUser: string, databaseUser: string,
databasePassword: string, databasePassword: string,
) => { ) => {
return `docker exec -i $CONTAINER_ID sh -c "mariadb -u ${databaseUser} -p${databasePassword} ${database}"`; return `docker exec -i $CONTAINER_ID sh -c "mariadb -u '${databaseUser}' -p'${databasePassword}' ${database}"`;
}; };
export const getMysqlRestoreCommand = ( export const getMysqlRestoreCommand = (
database: string, database: string,
databasePassword: string, databasePassword: string,
) => { ) => {
return `docker exec -i $CONTAINER_ID sh -c "mysql -u root -p${databasePassword} ${database}"`; return `docker exec -i $CONTAINER_ID sh -c "mysql -u root -p'${databasePassword}' ${database}"`;
}; };
export const getMongoRestoreCommand = ( export const getMongoRestoreCommand = (
@@ -30,7 +30,7 @@ export const getMongoRestoreCommand = (
databaseUser: string, databaseUser: string,
databasePassword: string, databasePassword: string,
) => { ) => {
return `docker exec -i $CONTAINER_ID sh -c "mongorestore --username ${databaseUser} --password ${databasePassword} --authenticationDatabase admin --db ${database} --archive"`; return `docker exec -i $CONTAINER_ID sh -c "mongorestore --username '${databaseUser}' --password '${databasePassword}' --authenticationDatabase admin --db ${database} --archive"`;
}; };
export const getComposeSearchCommand = ( export const getComposeSearchCommand = (
+8 -18
View File
@@ -347,8 +347,8 @@ importers:
specifier: ^0.2.1 specifier: ^0.2.1
version: 0.2.1([email protected](@opentelemetry/[email protected])([email protected]([email protected]))([email protected]))([email protected]([email protected]))([email protected]) version: 0.2.1([email protected](@opentelemetry/[email protected])([email protected]([email protected]))([email protected]))([email protected]([email protected]))([email protected])
node-os-utils: node-os-utils:
specifier: 1.3.7 specifier: 2.0.1
version: 1.3.7 version: 2.0.1
node-pty: node-pty:
specifier: 1.0.0 specifier: 1.0.0
version: 1.0.0 version: 1.0.0
@@ -473,9 +473,6 @@ importers:
'@types/node': '@types/node':
specifier: ^18.19.104 specifier: ^18.19.104
version: 18.19.104 version: 18.19.104
'@types/node-os-utils':
specifier: 1.3.4
version: 1.3.4
'@types/node-schedule': '@types/node-schedule':
specifier: 2.1.6 specifier: 2.1.6
version: 2.1.6 version: 2.1.6
@@ -688,8 +685,8 @@ importers:
specifier: 3.3.11 specifier: 3.3.11
version: 3.3.11 version: 3.3.11
node-os-utils: node-os-utils:
specifier: 1.3.7 specifier: 2.0.1
version: 1.3.7 version: 2.0.1
node-pty: node-pty:
specifier: 1.0.0 specifier: 1.0.0
version: 1.0.0 version: 1.0.0
@@ -766,9 +763,6 @@ importers:
'@types/node': '@types/node':
specifier: ^18.19.104 specifier: ^18.19.104
version: 18.19.104 version: 18.19.104
'@types/node-os-utils':
specifier: 1.3.4
version: 1.3.4
'@types/node-schedule': '@types/node-schedule':
specifier: 2.1.6 specifier: 2.1.6
version: 2.1.6 version: 2.1.6
@@ -4000,9 +3994,6 @@ packages:
'@types/[email protected]': '@types/[email protected]':
resolution: {integrity: sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==} resolution: {integrity: sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==}
'@types/[email protected]':
resolution: {integrity: sha512-BCUYrbdoO4FUbx6MB9atLNFnkxdliFaxdiTJMIPPiecXIApc5zf4NIqV5G1jWv/ReZvtYyHLs40RkBjHX+vykA==}
'@types/[email protected]': '@types/[email protected]':
resolution: {integrity: sha512-6AlZSUiNTdaVmH5jXYxX9YgmF1zfOlbjUqw0EllTBmZCnN1R5RR/m/u3No1OiWR05bnQ4jM4/+w4FcGvkAtnKQ==} resolution: {integrity: sha512-6AlZSUiNTdaVmH5jXYxX9YgmF1zfOlbjUqw0EllTBmZCnN1R5RR/m/u3No1OiWR05bnQ4jM4/+w4FcGvkAtnKQ==}
@@ -6312,8 +6303,9 @@ packages:
'@types/node': '@types/node':
optional: true optional: true
node-os-utils@1.3.7: node-os-utils@2.0.1:
resolution: {integrity: sha512-fvnX9tZbR7WfCG5BAy3yO/nCLyjVWD6MghEq0z5FDfN+ZXpLWNITBdbifxQkQ25ebr16G0N7eRWJisOcMEHG3Q==} resolution: {integrity: sha512-rH2N3qHZETLhdgTGhMMCE8zU3gsWO4we1MFtrSiAI7tYWrnJRc6dk2PseV4co3Lb0v/MbRONLQI2biHQYbpTpg==}
engines: {node: '>=18.0.0'}
[email protected]: [email protected]:
resolution: {integrity: sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==} resolution: {integrity: sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==}
@@ -11338,8 +11330,6 @@ snapshots:
dependencies: dependencies:
'@types/node': 20.17.51 '@types/node': 20.17.51
'@types/[email protected]': {}
'@types/[email protected]': '@types/[email protected]':
dependencies: dependencies:
'@types/node': 20.17.51 '@types/node': 20.17.51
@@ -13852,7 +13842,7 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/node': 18.19.104 '@types/node': 18.19.104
node-os-utils@1.3.7: {} node-os-utils@2.0.1: {}
[email protected]: [email protected]:
dependencies: dependencies: