Merge branch 'canary' into feat/github-triggerType

This commit is contained in:
Mauricio Siu 2025-04-06 15:09:45 -06:00
commit 4b15177260
31 changed files with 5806 additions and 368 deletions

View File

@ -14,6 +14,7 @@ import {
import { beforeEach, expect, test, vi } from "vitest"; import { beforeEach, expect, test, vi } from "vitest";
const baseAdmin: User = { const baseAdmin: User = {
https: false,
enablePaidFeatures: false, enablePaidFeatures: false,
metricsConfig: { metricsConfig: {
containers: { containers: {
@ -73,7 +74,6 @@ beforeEach(() => {
test("Should read the configuration file", () => { test("Should read the configuration file", () => {
const config: FileConfig = loadOrCreateConfig("dokploy"); const config: FileConfig = loadOrCreateConfig("dokploy");
expect(config.http?.routers?.["dokploy-router-app"]?.service).toBe( expect(config.http?.routers?.["dokploy-router-app"]?.service).toBe(
"dokploy-service-app", "dokploy-service-app",
); );
@ -83,6 +83,7 @@ test("Should apply redirect-to-https", () => {
updateServerTraefik( updateServerTraefik(
{ {
...baseAdmin, ...baseAdmin,
https: true,
certificateType: "letsencrypt", certificateType: "letsencrypt",
}, },
"example.com", "example.com",

View File

@ -0,0 +1,61 @@
import { describe, expect, test } from "vitest";
import { normalizeS3Path } from "@dokploy/server/utils/backups/utils";
describe("normalizeS3Path", () => {
test("should handle empty and whitespace-only prefix", () => {
expect(normalizeS3Path("")).toBe("");
expect(normalizeS3Path("/")).toBe("");
expect(normalizeS3Path(" ")).toBe("");
expect(normalizeS3Path("\t")).toBe("");
expect(normalizeS3Path("\n")).toBe("");
expect(normalizeS3Path(" \n \t ")).toBe("");
});
test("should trim whitespace from prefix", () => {
expect(normalizeS3Path(" prefix")).toBe("prefix/");
expect(normalizeS3Path("prefix ")).toBe("prefix/");
expect(normalizeS3Path(" prefix ")).toBe("prefix/");
expect(normalizeS3Path("\tprefix\t")).toBe("prefix/");
expect(normalizeS3Path(" prefix/nested ")).toBe("prefix/nested/");
});
test("should remove leading slashes", () => {
expect(normalizeS3Path("/prefix")).toBe("prefix/");
expect(normalizeS3Path("///prefix")).toBe("prefix/");
});
test("should remove trailing slashes", () => {
expect(normalizeS3Path("prefix/")).toBe("prefix/");
expect(normalizeS3Path("prefix///")).toBe("prefix/");
});
test("should remove both leading and trailing slashes", () => {
expect(normalizeS3Path("/prefix/")).toBe("prefix/");
expect(normalizeS3Path("///prefix///")).toBe("prefix/");
});
test("should handle nested paths", () => {
expect(normalizeS3Path("prefix/nested")).toBe("prefix/nested/");
expect(normalizeS3Path("/prefix/nested/")).toBe("prefix/nested/");
expect(normalizeS3Path("///prefix/nested///")).toBe("prefix/nested/");
});
test("should preserve middle slashes", () => {
expect(normalizeS3Path("prefix/nested/deep")).toBe("prefix/nested/deep/");
expect(normalizeS3Path("/prefix/nested/deep/")).toBe("prefix/nested/deep/");
});
test("should handle special characters", () => {
expect(normalizeS3Path("prefix-with-dashes")).toBe("prefix-with-dashes/");
expect(normalizeS3Path("prefix_with_underscores")).toBe(
"prefix_with_underscores/",
);
expect(normalizeS3Path("prefix.with.dots")).toBe("prefix.with.dots/");
});
test("should handle the cases from the bug report", () => {
expect(normalizeS3Path("instance-backups/")).toBe("instance-backups/");
expect(normalizeS3Path("/instance-backups/")).toBe("instance-backups/");
expect(normalizeS3Path("instance-backups")).toBe("instance-backups/");
});
});

View File

@ -307,7 +307,7 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => {
> >
{templates?.map((template) => ( {templates?.map((template) => (
<div <div
key={template.id} key={template?.id}
className={cn( className={cn(
"flex flex-col border rounded-lg overflow-hidden relative", "flex flex-col border rounded-lg overflow-hidden relative",
viewMode === "icon" && "h-[200px]", viewMode === "icon" && "h-[200px]",
@ -315,7 +315,7 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => {
)} )}
> >
<Badge className="absolute top-2 right-2" variant="blue"> <Badge className="absolute top-2 right-2" variant="blue">
{template.version} {template?.version}
</Badge> </Badge>
<div <div
className={cn( className={cn(
@ -324,21 +324,21 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => {
)} )}
> >
<img <img
src={`${customBaseUrl || "https://templates.dokploy.com/"}/blueprints/${template.id}/${template.logo}`} src={`${customBaseUrl || "https://templates.dokploy.com/"}/blueprints/${template?.id}/${template?.logo}`}
className={cn( className={cn(
"object-contain", "object-contain",
viewMode === "detailed" ? "size-24" : "size-16", viewMode === "detailed" ? "size-24" : "size-16",
)} )}
alt={template.name} alt={template?.name}
/> />
<div className="flex flex-col items-center gap-2"> <div className="flex flex-col items-center gap-2">
<span className="text-sm font-medium line-clamp-1"> <span className="text-sm font-medium line-clamp-1">
{template.name} {template?.name}
</span> </span>
{viewMode === "detailed" && {viewMode === "detailed" &&
template.tags.length > 0 && ( template?.tags?.length > 0 && (
<div className="flex flex-wrap justify-center gap-1.5"> <div className="flex flex-wrap justify-center gap-1.5">
{template.tags.map((tag) => ( {template?.tags?.map((tag) => (
<Badge <Badge
key={tag} key={tag}
variant="green" variant="green"
@ -356,7 +356,7 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => {
{viewMode === "detailed" && ( {viewMode === "detailed" && (
<ScrollArea className="flex-1 p-6"> <ScrollArea className="flex-1 p-6">
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">
{template.description} {template?.description}
</div> </div>
</ScrollArea> </ScrollArea>
)} )}
@ -372,25 +372,27 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => {
> >
{viewMode === "detailed" && ( {viewMode === "detailed" && (
<div className="flex gap-2"> <div className="flex gap-2">
<Link {template?.links?.github && (
href={template.links.github}
target="_blank"
className="text-muted-foreground hover:text-foreground transition-colors"
>
<GithubIcon className="size-5" />
</Link>
{template.links.website && (
<Link <Link
href={template.links.website} href={template?.links?.github}
target="_blank"
className="text-muted-foreground hover:text-foreground transition-colors"
>
<GithubIcon className="size-5" />
</Link>
)}
{template?.links?.website && (
<Link
href={template?.links?.website}
target="_blank" target="_blank"
className="text-muted-foreground hover:text-foreground transition-colors" className="text-muted-foreground hover:text-foreground transition-colors"
> >
<Globe className="size-5" /> <Globe className="size-5" />
</Link> </Link>
)} )}
{template.links.docs && ( {template?.links?.docs && (
<Link <Link
href={template.links.docs} href={template?.links?.docs}
target="_blank" target="_blank"
className="text-muted-foreground hover:text-foreground transition-colors" className="text-muted-foreground hover:text-foreground transition-colors"
> >
@ -419,7 +421,7 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => {
</AlertDialogTitle> </AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
This will create an application from the{" "} This will create an application from the{" "}
{template.name} template and add it to your {template?.name} template and add it to your
project. project.
</AlertDialogDescription> </AlertDialogDescription>

View File

@ -31,9 +31,14 @@ import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
const AddProjectSchema = z.object({ const AddProjectSchema = z.object({
name: z.string().min(1, { name: z
message: "Name is required", .string()
}), .min(1, {
message: "Name is required",
})
.regex(/^[a-zA-Z]/, {
message: "Project name cannot start with a number",
}),
description: z.string().optional(), description: z.string().optional(),
}); });
@ -97,18 +102,6 @@ export const HandleProject = ({ projectId }: Props) => {
); );
}); });
}; };
// useEffect(() => {
// const getUsers = async () => {
// const users = await authClient.admin.listUsers({
// query: {
// limit: 100,
// },
// });
// console.log(users);
// };
// getUsers();
// });
return ( return (
<Dialog open={isOpen} onOpenChange={setIsOpen}> <Dialog open={isOpen} onOpenChange={setIsOpen}>

View File

@ -115,7 +115,7 @@ export const ShowProjects = () => {
</span> </span>
</div> </div>
)} )}
<div className="w-full grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 3xl:grid-cols-5 flex-wrap gap-5"> <div className="w-full grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 3xl:grid-cols-5 flex-wrap gap-5">
{filteredProjects?.map((project) => { {filteredProjects?.map((project) => {
const emptyServices = const emptyServices =
project?.mariadb.length === 0 && project?.mariadb.length === 0 &&

View File

@ -9,6 +9,7 @@ import {
import { import {
Form, Form,
FormControl, FormControl,
FormDescription,
FormField, FormField,
FormItem, FormItem,
FormLabel, FormLabel,
@ -22,6 +23,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { GlobeIcon } from "lucide-react"; import { GlobeIcon } from "lucide-react";
@ -33,11 +35,19 @@ import { z } from "zod";
const addServerDomain = z const addServerDomain = z
.object({ .object({
domain: z.string().min(1, { message: "URL is required" }), domain: z.string(),
letsEncryptEmail: z.string(), letsEncryptEmail: z.string(),
https: z.boolean().optional(),
certificateType: z.enum(["letsencrypt", "none", "custom"]), certificateType: z.enum(["letsencrypt", "none", "custom"]),
}) })
.superRefine((data, ctx) => { .superRefine((data, ctx) => {
if (data.https && !data.certificateType) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["certificateType"],
message: "Required",
});
}
if (data.certificateType === "letsencrypt" && !data.letsEncryptEmail) { if (data.certificateType === "letsencrypt" && !data.letsEncryptEmail) {
ctx.addIssue({ ctx.addIssue({
code: z.ZodIssueCode.custom, code: z.ZodIssueCode.custom,
@ -61,15 +71,18 @@ export const WebDomain = () => {
domain: "", domain: "",
certificateType: "none", certificateType: "none",
letsEncryptEmail: "", letsEncryptEmail: "",
https: false,
}, },
resolver: zodResolver(addServerDomain), resolver: zodResolver(addServerDomain),
}); });
const https = form.watch("https");
useEffect(() => { useEffect(() => {
if (data) { if (data) {
form.reset({ form.reset({
domain: data?.user?.host || "", domain: data?.user?.host || "",
certificateType: data?.user?.certificateType, certificateType: data?.user?.certificateType,
letsEncryptEmail: data?.user?.letsEncryptEmail || "", letsEncryptEmail: data?.user?.letsEncryptEmail || "",
https: data?.user?.https || false,
}); });
} }
}, [form, form.reset, data]); }, [form, form.reset, data]);
@ -79,6 +92,7 @@ export const WebDomain = () => {
host: data.domain, host: data.domain,
letsEncryptEmail: data.letsEncryptEmail, letsEncryptEmail: data.letsEncryptEmail,
certificateType: data.certificateType, certificateType: data.certificateType,
https: data.https,
}) })
.then(async () => { .then(async () => {
await refetch(); await refetch();
@ -155,44 +169,67 @@ export const WebDomain = () => {
/> />
<FormField <FormField
control={form.control} control={form.control}
name="certificateType" name="https"
render={({ field }) => { render={({ field }) => (
return ( <FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm w-full col-span-2">
<FormItem className="md:col-span-2"> <div className="space-y-0.5">
<FormLabel> <FormLabel>HTTPS</FormLabel>
{t("settings.server.domain.form.certificate.label")} <FormDescription>
</FormLabel> Automatically provision SSL Certificate.
<Select </FormDescription>
onValueChange={field.onChange}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue
placeholder={t(
"settings.server.domain.form.certificate.placeholder",
)}
/>
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value={"none"}>
{t(
"settings.server.domain.form.certificateOptions.none",
)}
</SelectItem>
<SelectItem value={"letsencrypt"}>
{t(
"settings.server.domain.form.certificateOptions.letsencrypt",
)}
</SelectItem>
</SelectContent>
</Select>
<FormMessage /> <FormMessage />
</FormItem> </div>
); <FormControl>
}} <Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/> />
{https && (
<FormField
control={form.control}
name="certificateType"
render={({ field }) => {
return (
<FormItem className="md:col-span-2">
<FormLabel>
{t("settings.server.domain.form.certificate.label")}
</FormLabel>
<Select
onValueChange={field.onChange}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue
placeholder={t(
"settings.server.domain.form.certificate.placeholder",
)}
/>
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value={"none"}>
{t(
"settings.server.domain.form.certificateOptions.none",
)}
</SelectItem>
<SelectItem value={"letsencrypt"}>
{t(
"settings.server.domain.form.certificateOptions.letsencrypt",
)}
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
);
}}
/>
)}
<div className="flex w-full justify-end col-span-2"> <div className="flex w-full justify-end col-span-2">
<Button isLoading={isLoading} type="submit"> <Button isLoading={isLoading} type="submit">

View File

@ -120,17 +120,6 @@ export const UserNav = () => {
Docker Docker
</DropdownMenuItem> </DropdownMenuItem>
)} )}
{data?.role === "owner" && (
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
router.push("/dashboard/settings");
}}
>
Settings
</DropdownMenuItem>
)}
</> </>
) : ( ) : (
<> <>

View File

@ -0,0 +1 @@
ALTER TABLE "user_temp" ADD COLUMN "https" boolean DEFAULT false NOT NULL;

File diff suppressed because it is too large Load Diff

View File

@ -589,6 +589,13 @@
"when": 1743288371413, "when": 1743288371413,
"tag": "0083_parallel_stranger", "tag": "0083_parallel_stranger",
"breakpoints": true "breakpoints": true
},
{
"idx": 84,
"version": "7",
"when": 1743923992280,
"tag": "0084_thin_iron_lad",
"breakpoints": true
} }
] ]
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "dokploy", "name": "dokploy",
"version": "v0.21.2", "version": "v0.21.3",
"private": true, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",

View File

@ -314,31 +314,43 @@ const Project = (
}; };
const applicationActions = { const applicationActions = {
start: api.application.start.useMutation(),
stop: api.application.stop.useMutation(),
move: api.application.move.useMutation(), move: api.application.move.useMutation(),
delete: api.application.delete.useMutation(), delete: api.application.delete.useMutation(),
}; };
const postgresActions = { const postgresActions = {
start: api.postgres.start.useMutation(),
stop: api.postgres.stop.useMutation(),
move: api.postgres.move.useMutation(), move: api.postgres.move.useMutation(),
delete: api.postgres.remove.useMutation(), delete: api.postgres.remove.useMutation(),
}; };
const mysqlActions = { const mysqlActions = {
start: api.mysql.start.useMutation(),
stop: api.mysql.stop.useMutation(),
move: api.mysql.move.useMutation(), move: api.mysql.move.useMutation(),
delete: api.mysql.remove.useMutation(), delete: api.mysql.remove.useMutation(),
}; };
const mariadbActions = { const mariadbActions = {
start: api.mariadb.start.useMutation(),
stop: api.mariadb.stop.useMutation(),
move: api.mariadb.move.useMutation(), move: api.mariadb.move.useMutation(),
delete: api.mariadb.remove.useMutation(), delete: api.mariadb.remove.useMutation(),
}; };
const redisActions = { const redisActions = {
start: api.redis.start.useMutation(),
stop: api.redis.stop.useMutation(),
move: api.redis.move.useMutation(), move: api.redis.move.useMutation(),
delete: api.redis.remove.useMutation(), delete: api.redis.remove.useMutation(),
}; };
const mongoActions = { const mongoActions = {
start: api.mongo.start.useMutation(),
stop: api.mongo.stop.useMutation(),
move: api.mongo.move.useMutation(), move: api.mongo.move.useMutation(),
delete: api.mongo.remove.useMutation(), delete: api.mongo.remove.useMutation(),
}; };
@ -348,7 +360,32 @@ const Project = (
setIsBulkActionLoading(true); setIsBulkActionLoading(true);
for (const serviceId of selectedServices) { for (const serviceId of selectedServices) {
try { try {
await composeActions.start.mutateAsync({ composeId: serviceId }); const service = filteredServices.find((s) => s.id === serviceId);
if (!service) continue;
switch (service.type) {
case "application":
await applicationActions.start.mutateAsync({ applicationId: serviceId });
break;
case "compose":
await composeActions.start.mutateAsync({ composeId: serviceId });
break;
case "postgres":
await postgresActions.start.mutateAsync({ postgresId: serviceId });
break;
case "mysql":
await mysqlActions.start.mutateAsync({ mysqlId: serviceId });
break;
case "mariadb":
await mariadbActions.start.mutateAsync({ mariadbId: serviceId });
break;
case "redis":
await redisActions.start.mutateAsync({ redisId: serviceId });
break;
case "mongo":
await mongoActions.start.mutateAsync({ mongoId: serviceId });
break;
}
success++; success++;
} catch (_error) { } catch (_error) {
toast.error(`Error starting service ${serviceId}`); toast.error(`Error starting service ${serviceId}`);
@ -368,7 +405,32 @@ const Project = (
setIsBulkActionLoading(true); setIsBulkActionLoading(true);
for (const serviceId of selectedServices) { for (const serviceId of selectedServices) {
try { try {
await composeActions.stop.mutateAsync({ composeId: serviceId }); const service = filteredServices.find((s) => s.id === serviceId);
if (!service) continue;
switch (service.type) {
case "application":
await applicationActions.stop.mutateAsync({ applicationId: serviceId });
break;
case "compose":
await composeActions.stop.mutateAsync({ composeId: serviceId });
break;
case "postgres":
await postgresActions.stop.mutateAsync({ postgresId: serviceId });
break;
case "mysql":
await mysqlActions.stop.mutateAsync({ mysqlId: serviceId });
break;
case "mariadb":
await mariadbActions.stop.mutateAsync({ mariadbId: serviceId });
break;
case "redis":
await redisActions.stop.mutateAsync({ redisId: serviceId });
break;
case "mongo":
await mongoActions.stop.mutateAsync({ mongoId: serviceId });
break;
}
success++; success++;
} catch (_error) { } catch (_error) {
toast.error(`Error stopping service ${serviceId}`); toast.error(`Error stopping service ${serviceId}`);

View File

@ -1,219 +0,0 @@
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { DialogFooter } from "@/components/ui/dialog";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
} from "@/components/ui/form";
import { Switch } from "@/components/ui/switch";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server";
import { zodResolver } from "@hookform/resolvers/zod";
import { createServerSideHelpers } from "@trpc/react-query/server";
import { Settings } from "lucide-react";
import type { GetServerSidePropsContext } from "next";
import { type ReactElement, useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import superjson from "superjson";
import { z } from "zod";
const settings = z.object({
cleanCacheOnApplications: z.boolean(),
cleanCacheOnCompose: z.boolean(),
cleanCacheOnPreviews: z.boolean(),
});
type SettingsType = z.infer<typeof settings>;
const Page = () => {
const { data, refetch } = api.user.get.useQuery();
const { mutateAsync, isLoading, isError, error } =
api.user.update.useMutation();
const form = useForm<SettingsType>({
defaultValues: {
cleanCacheOnApplications: false,
cleanCacheOnCompose: false,
cleanCacheOnPreviews: false,
},
resolver: zodResolver(settings),
});
useEffect(() => {
form.reset({
cleanCacheOnApplications: data?.user.cleanupCacheApplications || false,
cleanCacheOnCompose: data?.user.cleanupCacheOnCompose || false,
cleanCacheOnPreviews: data?.user.cleanupCacheOnPreviews || false,
});
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
const onSubmit = async (values: SettingsType) => {
await mutateAsync({
cleanupCacheApplications: values.cleanCacheOnApplications,
cleanupCacheOnCompose: values.cleanCacheOnCompose,
cleanupCacheOnPreviews: values.cleanCacheOnPreviews,
})
.then(() => {
toast.success("Settings updated");
refetch();
})
.catch(() => {
toast.error("Something went wrong");
});
};
return (
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
<div className="rounded-xl bg-background shadow-md ">
<CardHeader className="">
<CardTitle className="text-xl flex flex-row gap-2">
<Settings className="size-6 text-muted-foreground self-center" />
Settings
</CardTitle>
<CardDescription>Manage your Dokploy settings</CardDescription>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
</CardHeader>
<CardContent className="space-y-2 py-8 border-t">
<Form {...form}>
<form
id="hook-form-add-security"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-2"
>
<FormField
control={form.control}
name="cleanCacheOnApplications"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
<div className="space-y-0.5">
<FormLabel>Clean Cache on Applications</FormLabel>
<FormDescription>
Clean the cache after every application deployment
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="cleanCacheOnPreviews"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
<div className="space-y-0.5">
<FormLabel>Clean Cache on Previews</FormLabel>
<FormDescription>
Clean the cache after every preview deployment
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="cleanCacheOnCompose"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
<div className="space-y-0.5">
<FormLabel>Clean Cache on Compose</FormLabel>
<FormDescription>
Clean the cache after every compose deployment
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<DialogFooter>
<Button
isLoading={isLoading}
form="hook-form-add-security"
type="submit"
>
Update
</Button>
</DialogFooter>
</form>
</Form>
</CardContent>
</div>
</Card>
</div>
);
};
export default Page;
Page.getLayout = (page: ReactElement) => {
return <DashboardLayout metaName="Server">{page}</DashboardLayout>;
};
export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
const { req, res } = ctx;
const { user, session } = await validateRequest(ctx.req);
if (!user) {
return {
redirect: {
permanent: true,
destination: "/",
},
};
}
if (user.role === "member") {
return {
redirect: {
permanent: true,
destination: "/dashboard/settings/profile",
},
};
}
const helpers = createServerSideHelpers({
router: appRouter,
ctx: {
req: req as any,
res: res as any,
db: null as any,
session: session as any,
user: user as any,
},
transformer: superjson,
});
await helpers.user.get.prefetch();
return {
props: {
trpcState: helpers.dehydrate(),
},
};
}

View File

@ -33,6 +33,7 @@ import {
findApplicationById, findApplicationById,
findProjectById, findProjectById,
getApplicationStats, getApplicationStats,
mechanizeDockerContainer,
readConfig, readConfig,
readRemoteConfig, readRemoteConfig,
removeDeployments, removeDeployments,
@ -132,28 +133,36 @@ export const applicationRouter = createTRPCRouter({
.input(apiReloadApplication) .input(apiReloadApplication)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId); const application = await findApplicationById(input.applicationId);
if (
application.project.organizationId !== ctx.session.activeOrganizationId try {
) { if (
application.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to reload this application",
});
}
if (application.serverId) {
await stopServiceRemote(application.serverId, input.appName);
} else {
await stopService(input.appName);
}
await updateApplicationStatus(input.applicationId, "idle");
await mechanizeDockerContainer(application);
await updateApplicationStatus(input.applicationId, "done");
return true;
} catch (error) {
await updateApplicationStatus(input.applicationId, "error");
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "INTERNAL_SERVER_ERROR",
message: "You are not authorized to reload this application", message: "Error reloading application",
cause: error,
}); });
} }
if (application.serverId) {
await stopServiceRemote(application.serverId, input.appName);
} else {
await stopService(input.appName);
}
await updateApplicationStatus(input.applicationId, "idle");
if (application.serverId) {
await startServiceRemote(application.serverId, input.appName);
} else {
await startService(input.appName);
}
await updateApplicationStatus(input.applicationId, "done");
return true;
}), }),
delete: protectedProcedure delete: protectedProcedure

View File

@ -31,7 +31,10 @@ import {
} from "@dokploy/server"; } from "@dokploy/server";
import { findDestinationById } from "@dokploy/server/services/destination"; import { findDestinationById } from "@dokploy/server/services/destination";
import { getS3Credentials } from "@dokploy/server/utils/backups/utils"; import {
getS3Credentials,
normalizeS3Path,
} from "@dokploy/server/utils/backups/utils";
import { import {
execAsync, execAsync,
execAsyncRemote, execAsyncRemote,
@ -257,7 +260,7 @@ export const backupRouter = createTRPCRouter({
const lastSlashIndex = input.search.lastIndexOf("/"); const lastSlashIndex = input.search.lastIndexOf("/");
const baseDir = const baseDir =
lastSlashIndex !== -1 lastSlashIndex !== -1
? input.search.slice(0, lastSlashIndex + 1) ? normalizeS3Path(input.search.slice(0, lastSlashIndex + 1))
: ""; : "";
const searchTerm = const searchTerm =
lastSlashIndex !== -1 lastSlashIndex !== -1
@ -270,7 +273,7 @@ export const backupRouter = createTRPCRouter({
let stdout = ""; let stdout = "";
if (input.serverId) { if (input.serverId) {
const result = await execAsyncRemote(listCommand, input.serverId); const result = await execAsyncRemote(input.serverId, listCommand);
stdout = result.stdout; stdout = result.stdout;
} else { } else {
const result = await execAsync(listCommand); const result = await execAsync(listCommand);

View File

@ -10,8 +10,8 @@ import {
import { import {
IS_CLOUD, IS_CLOUD,
createRegistry, createRegistry,
execAsync,
execAsyncRemote, execAsyncRemote,
execFileAsync,
findRegistryById, findRegistryById,
removeRegistry, removeRegistry,
updateRegistry, updateRegistry,
@ -83,7 +83,13 @@ export const registryRouter = createTRPCRouter({
.input(apiTestRegistry) .input(apiTestRegistry)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
try { try {
const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`; const args = [
"login",
input.registryUrl,
"--username",
input.username,
"--password-stdin",
];
if (IS_CLOUD && !input.serverId) { if (IS_CLOUD && !input.serverId) {
throw new TRPCError({ throw new TRPCError({
@ -93,9 +99,14 @@ export const registryRouter = createTRPCRouter({
} }
if (input.serverId && input.serverId !== "none") { if (input.serverId && input.serverId !== "none") {
await execAsyncRemote(input.serverId, loginCommand); await execAsyncRemote(
input.serverId,
`echo ${input.password} | docker ${args.join(" ")}`,
);
} else { } else {
await execAsync(loginCommand); await execFileAsync("docker", args, {
input: Buffer.from(input.password).toString(),
});
} }
return true; return true;

View File

@ -184,6 +184,7 @@ export const settingsRouter = createTRPCRouter({
letsEncryptEmail: input.letsEncryptEmail, letsEncryptEmail: input.letsEncryptEmail,
}), }),
certificateType: input.certificateType, certificateType: input.certificateType,
https: input.https,
}); });
if (!user) { if (!user) {

View File

@ -1,9 +1,9 @@
import type http from "node:http"; import type http from "node:http";
import { validateRequest } from "@dokploy/server/index";
import { applyWSSHandler } from "@trpc/server/adapters/ws"; import { applyWSSHandler } from "@trpc/server/adapters/ws";
import { WebSocketServer } from "ws"; import { WebSocketServer } from "ws";
import { appRouter } from "../api/root"; import { appRouter } from "../api/root";
import { createTRPCContext } from "../api/trpc"; import { createTRPCContext } from "../api/trpc";
import { validateRequest } from "@dokploy/server/lib/auth";
export const setupDrawerLogsWebSocketServer = ( export const setupDrawerLogsWebSocketServer = (
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>, server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
@ -13,11 +13,13 @@ export const setupDrawerLogsWebSocketServer = (
path: "/drawer-logs", path: "/drawer-logs",
}); });
// Set up tRPC WebSocket handler
applyWSSHandler({ applyWSSHandler({
wss: wssTerm, wss: wssTerm,
router: appRouter, router: appRouter,
createContext: createTRPCContext as any, createContext: createTRPCContext as any,
}); });
server.on("upgrade", (req, socket, head) => { server.on("upgrade", (req, socket, head) => {
const { pathname } = new URL(req.url || "", `http://${req.headers.host}`); const { pathname } = new URL(req.url || "", `http://${req.headers.host}`);

View File

@ -50,6 +50,7 @@ export const users_temp = pgTable("user_temp", {
// Admin // Admin
serverIp: text("serverIp"), serverIp: text("serverIp"),
certificateType: certificateType("certificateType").notNull().default("none"), certificateType: certificateType("certificateType").notNull().default("none"),
https: boolean("https").notNull().default(false),
host: text("host"), host: text("host"),
letsEncryptEmail: text("letsEncryptEmail"), letsEncryptEmail: text("letsEncryptEmail"),
sshPrivateKey: text("sshPrivateKey"), sshPrivateKey: text("sshPrivateKey"),
@ -202,10 +203,12 @@ export const apiAssignDomain = createSchema
host: true, host: true,
certificateType: true, certificateType: true,
letsEncryptEmail: true, letsEncryptEmail: true,
https: true,
}) })
.required() .required()
.partial({ .partial({
letsEncryptEmail: true, letsEncryptEmail: true,
https: true,
}); });
export const apiUpdateDockerCleanup = createSchema export const apiUpdateDockerCleanup = createSchema

View File

@ -1,3 +1,4 @@
import { faker } from "@faker-js/faker";
import type { Schema } from "./index"; import type { Schema } from "./index";
import { import {
generateBase64, generateBase64,
@ -70,7 +71,7 @@ function processValue(
schema: Schema, schema: Schema,
): string { ): string {
// First replace utility functions // First replace utility functions
let processedValue = value.replace(/\${([^}]+)}/g, (match, varName) => { let processedValue = value?.replace(/\${([^}]+)}/g, (match, varName) => {
// Handle utility functions // Handle utility functions
if (varName === "domain") { if (varName === "domain") {
return generateRandomDomain(schema); return generateRandomDomain(schema);
@ -117,6 +118,14 @@ function processValue(
return generateJwt(length); return generateJwt(length);
} }
if (varName === "username") {
return faker.internet.userName().toLowerCase();
}
if (varName === "email") {
return faker.internet.email().toLowerCase();
}
// If not a utility function, try to get from variables // If not a utility function, try to get from variables
return variables[varName] || match; return variables[varName] || match;
}); });
@ -177,7 +186,14 @@ export function processDomains(
variables: Record<string, string>, variables: Record<string, string>,
schema: Schema, schema: Schema,
): Template["domains"] { ): Template["domains"] {
if (!template?.config?.domains) return []; if (
!template?.config?.domains ||
template.config.domains.length === 0 ||
template.config.domains.every((domain) => !domain.serviceName)
) {
return [];
}
return template?.config?.domains?.map((domain: DomainConfig) => ({ return template?.config?.domains?.map((domain: DomainConfig) => ({
...domain, ...domain,
host: domain.host host: domain.host
@ -194,7 +210,9 @@ export function processEnvVars(
variables: Record<string, string>, variables: Record<string, string>,
schema: Schema, schema: Schema,
): Template["envs"] { ): Template["envs"] {
if (!template?.config?.env) return []; if (!template?.config?.env || Object.keys(template.config.env).length === 0) {
return [];
}
// Handle array of env vars // Handle array of env vars
if (Array.isArray(template.config.env)) { if (Array.isArray(template.config.env)) {
@ -233,7 +251,13 @@ export function processMounts(
variables: Record<string, string>, variables: Record<string, string>,
schema: Schema, schema: Schema,
): Template["mounts"] { ): Template["mounts"] {
if (!template?.config?.mounts) return []; if (
!template?.config?.mounts ||
template.config.mounts.length === 0 ||
template.config.mounts.every((mount) => !mount.filePath && !mount.content)
) {
return [];
}
return template?.config?.mounts?.map((mount: MountConfig) => ({ return template?.config?.mounts?.map((mount: MountConfig) => ({
filePath: processValue(mount.filePath, variables, schema), filePath: processValue(mount.filePath, variables, schema),

View File

@ -106,8 +106,8 @@ export const keepLatestNBackups = async (
backup.prefix, backup.prefix,
); );
// --include "*.sql.gz" ensures nothing else other than the db backup files are touched by rclone // --include "*.sql.gz" or "*.zip" ensures nothing else other than the dokploy backup files are touched by rclone
const rcloneList = `rclone lsf ${rcloneFlags.join(" ")} --include "*.sql.gz" ${backupFilesPath}`; const rcloneList = `rclone lsf ${rcloneFlags.join(" ")} --include "*${backup.databaseType === "web-server" ? ".zip" : ".sql.gz"}" ${backupFilesPath}`;
// when we pipe the above command with this one, we only get the list of files we want to delete // when we pipe the above command with this one, we only get the list of files we want to delete
const sortAndPickUnwantedBackups = `sort -r | tail -n +$((${backup.keepLatestCount}+1)) | xargs -I{}`; const sortAndPickUnwantedBackups = `sort -r | tail -n +$((${backup.keepLatestCount}+1)) | xargs -I{}`;
// this command deletes the files // this command deletes the files

View File

@ -1,4 +1,3 @@
import path from "node:path";
import type { BackupSchedule } from "@dokploy/server/services/backup"; import type { BackupSchedule } from "@dokploy/server/services/backup";
import type { Mariadb } from "@dokploy/server/services/mariadb"; import type { Mariadb } from "@dokploy/server/services/mariadb";
import { findProjectById } from "@dokploy/server/services/project"; import { findProjectById } from "@dokploy/server/services/project";
@ -8,7 +7,7 @@ import {
} from "../docker/utils"; } from "../docker/utils";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup"; import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync"; import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getS3Credentials } from "./utils"; import { getS3Credentials, normalizeS3Path } from "./utils";
export const runMariadbBackup = async ( export const runMariadbBackup = async (
mariadb: Mariadb, mariadb: Mariadb,
@ -19,7 +18,7 @@ export const runMariadbBackup = async (
const { prefix, database } = backup; const { prefix, database } = backup;
const destination = backup.destination; const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.sql.gz`; const backupFileName = `${new Date().toISOString()}.sql.gz`;
const bucketDestination = path.join(prefix, backupFileName); const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
try { try {
const rcloneFlags = getS3Credentials(destination); const rcloneFlags = getS3Credentials(destination);

View File

@ -1,4 +1,3 @@
import path from "node:path";
import type { BackupSchedule } from "@dokploy/server/services/backup"; import type { BackupSchedule } from "@dokploy/server/services/backup";
import type { Mongo } from "@dokploy/server/services/mongo"; import type { Mongo } from "@dokploy/server/services/mongo";
import { findProjectById } from "@dokploy/server/services/project"; import { findProjectById } from "@dokploy/server/services/project";
@ -8,7 +7,7 @@ import {
} from "../docker/utils"; } from "../docker/utils";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup"; import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync"; import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getS3Credentials } from "./utils"; import { getS3Credentials, normalizeS3Path } from "./utils";
// mongodb://mongo:Bqh7AQl-PRbnBu@localhost:27017/?tls=false&directConnection=true // mongodb://mongo:Bqh7AQl-PRbnBu@localhost:27017/?tls=false&directConnection=true
export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => { export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
@ -17,7 +16,7 @@ export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
const { prefix, database } = backup; const { prefix, database } = backup;
const destination = backup.destination; const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.dump.gz`; const backupFileName = `${new Date().toISOString()}.dump.gz`;
const bucketDestination = path.join(prefix, backupFileName); const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
try { try {
const rcloneFlags = getS3Credentials(destination); const rcloneFlags = getS3Credentials(destination);

View File

@ -1,4 +1,3 @@
import path from "node:path";
import type { BackupSchedule } from "@dokploy/server/services/backup"; import type { BackupSchedule } from "@dokploy/server/services/backup";
import type { MySql } from "@dokploy/server/services/mysql"; import type { MySql } from "@dokploy/server/services/mysql";
import { findProjectById } from "@dokploy/server/services/project"; import { findProjectById } from "@dokploy/server/services/project";
@ -8,7 +7,7 @@ import {
} from "../docker/utils"; } from "../docker/utils";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup"; import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync"; import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getS3Credentials } from "./utils"; import { getS3Credentials, normalizeS3Path } from "./utils";
export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => { export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => {
const { appName, databaseRootPassword, projectId, name } = mysql; const { appName, databaseRootPassword, projectId, name } = mysql;
@ -16,7 +15,7 @@ export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => {
const { prefix, database } = backup; const { prefix, database } = backup;
const destination = backup.destination; const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.sql.gz`; const backupFileName = `${new Date().toISOString()}.sql.gz`;
const bucketDestination = path.join(prefix, backupFileName); const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
try { try {
const rcloneFlags = getS3Credentials(destination); const rcloneFlags = getS3Credentials(destination);

View File

@ -1,4 +1,3 @@
import path from "node:path";
import type { BackupSchedule } from "@dokploy/server/services/backup"; import type { BackupSchedule } from "@dokploy/server/services/backup";
import type { Postgres } from "@dokploy/server/services/postgres"; import type { Postgres } from "@dokploy/server/services/postgres";
import { findProjectById } from "@dokploy/server/services/project"; import { findProjectById } from "@dokploy/server/services/project";
@ -8,7 +7,7 @@ import {
} from "../docker/utils"; } from "../docker/utils";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup"; import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync"; import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getS3Credentials } from "./utils"; import { getS3Credentials, normalizeS3Path } from "./utils";
export const runPostgresBackup = async ( export const runPostgresBackup = async (
postgres: Postgres, postgres: Postgres,
@ -20,7 +19,7 @@ export const runPostgresBackup = async (
const { prefix, database } = backup; const { prefix, database } = backup;
const destination = backup.destination; const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.sql.gz`; const backupFileName = `${new Date().toISOString()}.sql.gz`;
const bucketDestination = path.join(prefix, backupFileName); const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
try { try {
const rcloneFlags = getS3Credentials(destination); const rcloneFlags = getS3Credentials(destination);
const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`; const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`;

View File

@ -36,6 +36,13 @@ export const removeScheduleBackup = (backupId: string) => {
currentJob?.cancel(); currentJob?.cancel();
}; };
export const normalizeS3Path = (prefix: string) => {
// Trim whitespace and remove leading/trailing slashes
const normalizedPrefix = prefix.trim().replace(/^\/+|\/+$/g, "");
// Return empty string if prefix is empty, otherwise append trailing slash
return normalizedPrefix ? `${normalizedPrefix}/` : "";
};
export const getS3Credentials = (destination: Destination) => { export const getS3Credentials = (destination: Destination) => {
const { accessKey, secretAccessKey, region, endpoint, provider } = const { accessKey, secretAccessKey, region, endpoint, provider } =
destination; destination;

View File

@ -1,6 +1,6 @@
import type { BackupSchedule } from "@dokploy/server/services/backup"; import type { BackupSchedule } from "@dokploy/server/services/backup";
import { execAsync } from "../process/execAsync"; import { execAsync } from "../process/execAsync";
import { getS3Credentials } from "./utils"; import { getS3Credentials, normalizeS3Path } from "./utils";
import { findDestinationById } from "@dokploy/server/services/destination"; import { findDestinationById } from "@dokploy/server/services/destination";
import { IS_CLOUD, paths } from "@dokploy/server/constants"; import { IS_CLOUD, paths } from "@dokploy/server/constants";
import { mkdtemp } from "node:fs/promises"; import { mkdtemp } from "node:fs/promises";
@ -18,7 +18,7 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
const { BASE_PATH } = paths(); const { BASE_PATH } = paths();
const tempDir = await mkdtemp(join(tmpdir(), "dokploy-backup-")); const tempDir = await mkdtemp(join(tmpdir(), "dokploy-backup-"));
const backupFileName = `webserver-backup-${timestamp}.zip`; const backupFileName = `webserver-backup-${timestamp}.zip`;
const s3Path = `:s3:${destination.bucket}/${backup.prefix}${backupFileName}`; const s3Path = `:s3:${destination.bucket}/${normalizeS3Path(backup.prefix)}${backupFileName}`;
try { try {
await execAsync(`mkdir -p ${tempDir}/filesystem`); await execAsync(`mkdir -p ${tempDir}/filesystem`);
@ -29,7 +29,7 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
await execAsync(`cp -r ${BASE_PATH}/* ${tempDir}/filesystem/`); await execAsync(`cp -r ${BASE_PATH}/* ${tempDir}/filesystem/`);
await execAsync( await execAsync(
`cd ${tempDir} && zip -r ${backupFileName} database.sql filesystem/`, `cd ${tempDir} && zip -r ${backupFileName} database.sql filesystem/ > /dev/null 2>&1`,
); );
const uploadCommand = `rclone copyto ${rcloneFlags.join(" ")} "${tempDir}/${backupFileName}" "${s3Path}"`; const uploadCommand = `rclone copyto ${rcloneFlags.join(" ")} "${tempDir}/${backupFileName}" "${s3Path}"`;

View File

@ -25,6 +25,12 @@ export const buildStatic = async (
].join("\n"), ].join("\n"),
); );
createFile(
buildAppDirectory,
".dockerignore",
[".git", ".env", "Dockerfile", ".dockerignore"].join("\n"),
);
await buildCustomDocker( await buildCustomDocker(
{ {
...application, ...application,

View File

@ -1,9 +1,48 @@
import { exec } from "node:child_process"; import { exec, execFile } from "node:child_process";
import util from "node:util"; import util from "node:util";
import { findServerById } from "@dokploy/server/services/server"; import { findServerById } from "@dokploy/server/services/server";
import { Client } from "ssh2"; import { Client } from "ssh2";
export const execAsync = util.promisify(exec); export const execAsync = util.promisify(exec);
export const execFileAsync = async (
command: string,
args: string[],
options: { input?: string } = {},
): Promise<{ stdout: string; stderr: string }> => {
const child = execFile(command, args);
if (options.input && child.stdin) {
child.stdin.write(options.input);
child.stdin.end();
}
return new Promise((resolve, reject) => {
let stdout = "";
let stderr = "";
child.stdout?.on("data", (data) => {
stdout += data.toString();
});
child.stderr?.on("data", (data) => {
stderr += data.toString();
});
child.on("close", (code) => {
if (code === 0) {
resolve({ stdout, stderr });
} else {
reject(
new Error(`Command failed with code ${code}. Stderr: ${stderr}`),
);
}
});
child.on("error", reject);
});
};
export const execAsyncRemote = async ( export const execAsyncRemote = async (
serverId: string | null, serverId: string | null,
command: string, command: string,

View File

@ -45,7 +45,7 @@ export const restoreWebServerBackup = async (
// Extract backup // Extract backup
emit("Extracting backup..."); emit("Extracting backup...");
await execAsync(`cd ${tempDir} && unzip ${backupFile}`); await execAsync(`cd ${tempDir} && unzip ${backupFile} > /dev/null 2>&1`);
// Restore filesystem first // Restore filesystem first
emit("Restoring filesystem..."); emit("Restoring filesystem...");

View File

@ -3,7 +3,11 @@ import { join } from "node:path";
import { paths } from "@dokploy/server/constants"; import { paths } from "@dokploy/server/constants";
import type { User } from "@dokploy/server/services/user"; import type { User } from "@dokploy/server/services/user";
import { dump, load } from "js-yaml"; import { dump, load } from "js-yaml";
import { loadOrCreateConfig, writeTraefikConfig } from "./application"; import {
loadOrCreateConfig,
removeTraefikConfig,
writeTraefikConfig,
} from "./application";
import type { FileConfig } from "./file-types"; import type { FileConfig } from "./file-types";
import type { MainTraefikConfig } from "./types"; import type { MainTraefikConfig } from "./types";
@ -11,32 +15,62 @@ export const updateServerTraefik = (
user: User | null, user: User | null,
newHost: string | null, newHost: string | null,
) => { ) => {
const { https, certificateType } = user || {};
const appName = "dokploy"; const appName = "dokploy";
const config: FileConfig = loadOrCreateConfig(appName); const config: FileConfig = loadOrCreateConfig(appName);
config.http = config.http || { routers: {}, services: {} }; config.http = config.http || { routers: {}, services: {} };
config.http.routers = config.http.routers || {}; config.http.routers = config.http.routers || {};
config.http.services = config.http.services || {};
const currentRouterConfig = config.http.routers[`${appName}-router-app`]; const currentRouterConfig = config.http.routers[`${appName}-router-app`] || {
rule: `Host(\`${newHost}\`)`,
service: `${appName}-service-app`,
entryPoints: ["web"],
};
config.http.routers[`${appName}-router-app`] = currentRouterConfig;
if (currentRouterConfig && newHost) { config.http.services = {
currentRouterConfig.rule = `Host(\`${newHost}\`)`; ...config.http.services,
[`${appName}-service-app`]: {
loadBalancer: {
servers: [
{
url: `http://dokploy:${process.env.PORT || 3000}`,
passHostHeader: true,
},
],
},
},
};
if (user?.certificateType === "letsencrypt") { if (https) {
currentRouterConfig.middlewares = ["redirect-to-https"];
if (certificateType === "letsencrypt") {
config.http.routers[`${appName}-router-app-secure`] = { config.http.routers[`${appName}-router-app-secure`] = {
...currentRouterConfig, rule: `Host(\`${newHost}\`)`,
service: `${appName}-service-app`,
entryPoints: ["websecure"], entryPoints: ["websecure"],
tls: { certResolver: "letsencrypt" }, tls: { certResolver: "letsencrypt" },
}; };
currentRouterConfig.middlewares = ["redirect-to-https"];
} else { } else {
delete config.http.routers[`${appName}-router-app-secure`]; config.http.routers[`${appName}-router-app-secure`] = {
currentRouterConfig.middlewares = []; rule: `Host(\`${newHost}\`)`,
service: `${appName}-service-app`,
entryPoints: ["websecure"],
};
} }
} else {
delete config.http.routers[`${appName}-router-app-secure`];
currentRouterConfig.middlewares = [];
} }
writeTraefikConfig(config, appName); if (newHost) {
writeTraefikConfig(config, appName);
} else {
removeTraefikConfig(appName);
}
}; };
export const updateLetsEncryptEmail = (newEmail: string | null) => { export const updateLetsEncryptEmail = (newEmail: string | null) => {