Compare commits

...

23 Commits

Author SHA1 Message Date
Mauricio Siu
7fe163dd33 Merge pull request #1913 from Dokploy/feat/add-appname-compose
Some checks are pending
Auto PR to main when version changes / create-pr (push) Waiting to run
Build Docker images / build-and-push-cloud-image (push) Waiting to run
Build Docker images / build-and-push-schedule-image (push) Waiting to run
Build Docker images / build-and-push-server-image (push) Waiting to run
Dokploy Docker Build / docker-amd (push) Waiting to run
Dokploy Docker Build / docker-arm (push) Waiting to run
Dokploy Docker Build / combine-manifests (push) Blocked by required conditions
Dokploy Docker Build / generate-release (push) Blocked by required conditions
autofix.ci / format (push) Waiting to run
Dokploy Monitoring Build / docker-amd (push) Waiting to run
Dokploy Monitoring Build / docker-arm (push) Waiting to run
Dokploy Monitoring Build / combine-manifests (push) Blocked by required conditions
Feat/add appname compose
2025-05-17 15:37:22 -06:00
Mauricio Siu
19b56771b8 style: update styling for environment display and increase scroll area height in import component 2025-05-17 15:28:52 -06:00
Mauricio Siu
cff01ed438 refactor: modify template processing to include APP_NAME variable in configuration 2025-05-17 15:27:42 -06:00
Mauricio Siu
10fa3c8cf1 fix: update environment file generation to include APP_NAME variable 2025-05-17 15:18:31 -06:00
Mauricio Siu
6c5497ed21 Merge pull request #1912 from Dokploy/1873-duplicate-clones-the-project-not-the-service
feat: enhance project duplication functionality with options for new …
2025-05-17 14:35:17 -06:00
Mauricio Siu
380656efee feat: enhance project duplication functionality with options for new or same project 2025-05-17 14:33:07 -06:00
Mauricio Siu
c64d2245ce Merge pull request #1897 from enie123/canary
Some checks are pending
Auto PR to main when version changes / create-pr (push) Waiting to run
Build Docker images / build-and-push-cloud-image (push) Waiting to run
Build Docker images / build-and-push-schedule-image (push) Waiting to run
Build Docker images / build-and-push-server-image (push) Waiting to run
Dokploy Docker Build / docker-amd (push) Waiting to run
Dokploy Docker Build / docker-arm (push) Waiting to run
Dokploy Docker Build / combine-manifests (push) Blocked by required conditions
Dokploy Docker Build / generate-release (push) Blocked by required conditions
autofix.ci / format (push) Waiting to run
Dokploy Monitoring Build / docker-amd (push) Waiting to run
Dokploy Monitoring Build / docker-arm (push) Waiting to run
Dokploy Monitoring Build / combine-manifests (push) Blocked by required conditions
build: update nixpacks to 1.39.0
2025-05-17 03:54:11 -06:00
Mauricio Siu
a985998b93 feat: add VPS provider recommendations and alerts in server settings 2025-05-17 03:53:37 -06:00
Mauricio Siu
4f3ba16dfa chore: bump version to v0.22.5 in package.json 2025-05-17 03:34:31 -06:00
Mauricio Siu
6c788429f1 Merge pull request #1910 from Dokploy/1894-gitlab-self-hosted-cannot-find-all-repository
refactor: streamline GitLab repository fetching by introducing valida…
2025-05-17 03:02:26 -06:00
Mauricio Siu
3176a9d7e3 refactor: streamline GitLab repository fetching by introducing validateGitlabProvider function for improved error handling and pagination 2025-05-17 02:51:40 -06:00
Mauricio Siu
94a6a9587e Merge pull request #1872 from IPdotSetAF/git-lfs-not-supported
fix: installed git-lfs in docker image
2025-05-17 02:09:03 -06:00
Mauricio Siu
911681f389 fix: add git-lfs installation to various OS setups in server-setup script 2025-05-17 02:03:04 -06:00
Mauricio Siu
5992688e85 Merge pull request #1909 from Dokploy/1868-backups-failing-due-to-a-directory-not-empty-error
refactor: improve cleanup process in web server backup utility to han…
2025-05-17 00:21:58 -06:00
Mauricio Siu
425061e481 refactor: improve cleanup process in web server backup utility to handle errors during temporary directory removal 2025-05-17 00:20:54 -06:00
Mauricio Siu
08c0bf8a21 Merge pull request #1908 from Dokploy/1863-pg_dump-backup-fails-stdout-maxbuffer-length-exceeded
refactor: update database backup process in web server utility to use…
2025-05-17 00:14:59 -06:00
Mauricio Siu
64a2c9e0a1 refactor: update database backup process in web server utility to use temporary file in container 2025-05-17 00:13:43 -06:00
Mauricio Siu
21e46f5382 Merge pull request #1907 from Dokploy/1878-dokploy-application-settings-not-linked-resulting-in-progress-lost-on-save
fix: update dependencies in save provider components to use optional …
2025-05-16 23:23:20 -06:00
autofix-ci[bot]
52b2158309 [autofix.ci] apply automated fixes 2025-05-17 05:22:55 +00:00
Mauricio Siu
178d84d438 fix: update dependencies in save provider components to use optional chaining for applicationId and composeId 2025-05-16 23:22:26 -06:00
Mauricio Siu
80016b57a8 Merge pull request #1906 from Dokploy/1888-docker-compose-preview-is-null
feat(ui): add loading state and no data message to converted compose …
2025-05-16 23:16:29 -06:00
Eric Nie
d4032f34bf build: update nixpacks to 1.39.0 2025-05-15 00:45:48 +07:00
IPdotSetAF
5156b45ffc fix: installed git-lfs in docker image 2025-05-11 10:36:52 +03:30
24 changed files with 299 additions and 117 deletions

View File

@@ -49,7 +49,7 @@ RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm
# Install Nixpacks and tsx # Install Nixpacks and tsx
# | VERBOSE=1 VERSION=1.21.0 bash # | VERBOSE=1 VERSION=1.21.0 bash
ARG NIXPACKS_VERSION=1.35.0 ARG NIXPACKS_VERSION=1.39.0
RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \ RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
&& chmod +x install.sh \ && chmod +x install.sh \
&& ./install.sh \ && ./install.sh \

View File

@@ -8,7 +8,7 @@ COPY . /usr/src/app
WORKDIR /usr/src/app WORKDIR /usr/src/app
RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/* RUN apt-get update && apt-get install -y python3 make g++ git git-lfs && git lfs install && rm -rf /var/lib/apt/lists/*
# Install dependencies # Install dependencies
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile

View File

@@ -263,7 +263,7 @@ export const ShowImport = ({ composeId }: Props) => {
{templateInfo.template.envs.map((env, index) => ( {templateInfo.template.envs.map((env, index) => (
<div <div
key={index} key={index}
className="rounded-lg border bg-card p-2 font-mono text-sm" className="rounded-lg truncate border bg-card p-2 font-mono text-sm"
> >
{env} {env}
</div> </div>
@@ -328,7 +328,7 @@ export const ShowImport = ({ composeId }: Props) => {
<DialogDescription>Mount File Content</DialogDescription> <DialogDescription>Mount File Content</DialogDescription>
</DialogHeader> </DialogHeader>
<ScrollArea className="h-[25vh] pr-4"> <ScrollArea className="h-[45vh] pr-4">
<CodeEditor <CodeEditor
language="yaml" language="yaml"
value={selectedMount?.content || ""} value={selectedMount?.content || ""}

View File

@@ -86,7 +86,7 @@ export const ShowDeployments = ({
<span>Webhook URL: </span> <span>Webhook URL: </span>
<div className="flex flex-row items-center gap-2"> <div className="flex flex-row items-center gap-2">
<span className="break-all text-muted-foreground"> <span className="break-all text-muted-foreground">
{`${url}/api/deploy${type === 'compose' ? '/compose' : ''}/${refreshToken}`} {`${url}/api/deploy${type === "compose" ? "/compose" : ""}/${refreshToken}`}
</span> </span>
{(type === "application" || type === "compose") && ( {(type === "application" || type === "compose") && (
<RefreshToken id={id} type={type} /> <RefreshToken id={id} type={type} />

View File

@@ -136,7 +136,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
enableSubmodules: data.enableSubmodules || false, enableSubmodules: data.enableSubmodules || false,
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data?.applicationId, form]);
const onSubmit = async (data: BitbucketProvider) => { const onSubmit = async (data: BitbucketProvider) => {
await mutateAsync({ await mutateAsync({

View File

@@ -53,7 +53,7 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
registryURL: data.registryUrl || "", registryURL: data.registryUrl || "",
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data?.applicationId, form]);
const onSubmit = async (values: DockerProvider) => { const onSubmit = async (values: DockerProvider) => {
await mutateAsync({ await mutateAsync({

View File

@@ -158,7 +158,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
enableSubmodules: data.enableSubmodules || false, enableSubmodules: data.enableSubmodules || false,
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data?.applicationId, form]);
const onSubmit = async (data: GiteaProvider) => { const onSubmit = async (data: GiteaProvider) => {
await mutateAsync({ await mutateAsync({

View File

@@ -134,7 +134,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
enableSubmodules: data.enableSubmodules ?? false, enableSubmodules: data.enableSubmodules ?? false,
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data?.applicationId, form]);
const onSubmit = async (data: GithubProvider) => { const onSubmit = async (data: GithubProvider) => {
await mutateAsync({ await mutateAsync({

View File

@@ -141,7 +141,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
enableSubmodules: data.enableSubmodules ?? false, enableSubmodules: data.enableSubmodules ?? false,
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data?.applicationId, form]);
const onSubmit = async (data: GitlabProvider) => { const onSubmit = async (data: GitlabProvider) => {
await mutateAsync({ await mutateAsync({

View File

@@ -136,7 +136,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
enableSubmodules: data.enableSubmodules ?? false, enableSubmodules: data.enableSubmodules ?? false,
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data?.composeId, form]);
const onSubmit = async (data: BitbucketProvider) => { const onSubmit = async (data: BitbucketProvider) => {
await mutateAsync({ await mutateAsync({

View File

@@ -142,7 +142,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
enableSubmodules: data.enableSubmodules ?? false, enableSubmodules: data.enableSubmodules ?? false,
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data?.composeId, form]);
const onSubmit = async (data: GiteaProvider) => { const onSubmit = async (data: GiteaProvider) => {
await mutateAsync({ await mutateAsync({

View File

@@ -134,7 +134,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
enableSubmodules: data.enableSubmodules ?? false, enableSubmodules: data.enableSubmodules ?? false,
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data?.composeId, form]);
const onSubmit = async (data: GithubProvider) => { const onSubmit = async (data: GithubProvider) => {
await mutateAsync({ await mutateAsync({

View File

@@ -142,7 +142,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
enableSubmodules: data.enableSubmodules ?? false, enableSubmodules: data.enableSubmodules ?? false,
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data?.composeId, form]);
const onSubmit = async (data: GitlabProvider) => { const onSubmit = async (data: GitlabProvider) => {
await mutateAsync({ await mutateAsync({

View File

@@ -15,6 +15,7 @@ import { Copy, Loader2 } from "lucide-react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
export type Services = { export type Services = {
appName: string; appName: string;
@@ -48,6 +49,7 @@ export const DuplicateProject = ({
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [name, setName] = useState(""); const [name, setName] = useState("");
const [description, setDescription] = useState(""); const [description, setDescription] = useState("");
const [duplicateType, setDuplicateType] = useState("new-project"); // "new-project" or "same-project"
const utils = api.useUtils(); const utils = api.useUtils();
const router = useRouter(); const router = useRouter();
@@ -59,9 +61,15 @@ export const DuplicateProject = ({
api.project.duplicate.useMutation({ api.project.duplicate.useMutation({
onSuccess: async (newProject) => { onSuccess: async (newProject) => {
await utils.project.all.invalidate(); await utils.project.all.invalidate();
toast.success("Project duplicated successfully"); toast.success(
duplicateType === "new-project"
? "Project duplicated successfully"
: "Services duplicated successfully",
);
setOpen(false); setOpen(false);
router.push(`/dashboard/project/${newProject.projectId}`); if (duplicateType === "new-project") {
router.push(`/dashboard/project/${newProject.projectId}`);
}
}, },
onError: (error) => { onError: (error) => {
toast.error(error.message); toast.error(error.message);
@@ -69,7 +77,7 @@ export const DuplicateProject = ({
}); });
const handleDuplicate = async () => { const handleDuplicate = async () => {
if (!name) { if (duplicateType === "new-project" && !name) {
toast.error("Project name is required"); toast.error("Project name is required");
return; return;
} }
@@ -83,6 +91,7 @@ export const DuplicateProject = ({
id: service.id, id: service.id,
type: service.type, type: service.type,
})), })),
duplicateInSameProject: duplicateType === "same-project",
}); });
}; };
@@ -95,6 +104,7 @@ export const DuplicateProject = ({
// Reset form when closing // Reset form when closing
setName(""); setName("");
setDescription(""); setDescription("");
setDuplicateType("new-project");
} }
}} }}
> >
@@ -106,32 +116,54 @@ export const DuplicateProject = ({
</DialogTrigger> </DialogTrigger>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>Duplicate Project</DialogTitle> <DialogTitle>Duplicate Services</DialogTitle>
<DialogDescription> <DialogDescription>
Create a new project with the selected services Choose where to duplicate the selected services
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="grid gap-4 py-4"> <div className="grid gap-4 py-4">
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="name">Name</Label> <Label>Duplicate to</Label>
<Input <RadioGroup
id="name" value={duplicateType}
value={name} onValueChange={setDuplicateType}
onChange={(e) => setName(e.target.value)} className="grid gap-2"
placeholder="New project name" >
/> <div className="flex items-center space-x-2">
<RadioGroupItem value="new-project" id="new-project" />
<Label htmlFor="new-project">New project</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="same-project" id="same-project" />
<Label htmlFor="same-project">Same project</Label>
</div>
</RadioGroup>
</div> </div>
<div className="grid gap-2"> {duplicateType === "new-project" && (
<Label htmlFor="description">Description</Label> <>
<Input <div className="grid gap-2">
id="description" <Label htmlFor="name">Name</Label>
value={description} <Input
onChange={(e) => setDescription(e.target.value)} id="name"
placeholder="Project description (optional)" value={name}
/> onChange={(e) => setName(e.target.value)}
</div> placeholder="New project name"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="description">Description</Label>
<Input
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Project description (optional)"
/>
</div>
</>
)}
<div className="grid gap-2"> <div className="grid gap-2">
<Label>Selected services to duplicate</Label> <Label>Selected services to duplicate</Label>
@@ -159,10 +191,14 @@ export const DuplicateProject = ({
{isLoading ? ( {isLoading ? (
<> <>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="mr-2 h-4 w-4 animate-spin" />
Duplicating... {duplicateType === "new-project"
? "Duplicating project..."
: "Duplicating services..."}
</> </>
) : duplicateType === "new-project" ? (
"Duplicate project"
) : ( ) : (
"Duplicate" "Duplicate services"
)} )}
</Button> </Button>
</DialogFooter> </DialogFooter>

View File

@@ -156,6 +156,67 @@ export const HandleServers = ({ serverId }: Props) => {
remotely. remotely.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div>
<p className="text-primary text-sm font-medium">
You will need to purchase or rent a Virtual Private Server (VPS) to
proceed, we recommend to use one of these providers since has been
heavily tested.
</p>
<ul className="list-inside list-disc pl-4 text-sm text-muted-foreground mt-4">
<li>
<a
href="https://www.hostinger.com/vps-hosting?REFERRALCODE=1SIUMAURICI97"
className="text-link underline"
>
Hostinger - Get 20% Discount
</a>
</li>
<li>
<a
href=" https://app.americancloud.com/register?ref=dokploy"
className="text-link underline"
>
American Cloud - Get $20 Credits
</a>
</li>
<li>
<a
href="https://m.do.co/c/db24efd43f35"
className="text-link underline"
>
DigitalOcean - Get $200 Credits
</a>
</li>
<li>
<a
href="https://hetzner.cloud/?ref=vou4fhxJ1W2D"
className="text-link underline"
>
Hetzner - Get 20 Credits
</a>
</li>
<li>
<a
href="https://www.vultr.com/?ref=9679828"
className="text-link underline"
>
Vultr
</a>
</li>
<li>
<a
href="https://www.linode.com/es/pricing/#compute-shared"
className="text-link underline"
>
Linode
</a>
</li>
</ul>
<AlertBlock className="mt-4 px-4">
You are free to use whatever provider, but we recommend to use one
of the above, to avoid issues.
</AlertBlock>
</div>
{!canCreateMoreServers && ( {!canCreateMoreServers && (
<AlertBlock type="warning"> <AlertBlock type="warning">
You cannot create more servers,{" "} You cannot create more servers,{" "}

View File

@@ -177,6 +177,14 @@ export const WelcomeSuscription = () => {
Hostinger - Get 20% Discount Hostinger - Get 20% Discount
</a> </a>
</li> </li>
<li>
<a
href=" https://app.americancloud.com/register?ref=dokploy"
className="text-link underline"
>
American Cloud - Get $20 Credits
</a>
</li>
<li> <li>
<a <a
href="https://m.do.co/c/db24efd43f35" href="https://m.do.co/c/db24efd43f35"

View File

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

View File

@@ -80,7 +80,11 @@ export default function Custom404({ statusCode, error }: Props) {
<footer className="mt-auto text-center py-5"> <footer className="mt-auto text-center py-5">
<div className="max-w-[85rem] mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-[85rem] mx-auto px-4 sm:px-6 lg:px-8">
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
<Link href="https://github.com/Dokploy/dokploy/issues" target="_blank" className="underline hover:text-primary transition-colors" > <Link
href="https://github.com/Dokploy/dokploy/issues"
target="_blank"
className="underline hover:text-primary transition-colors"
>
Submit Log in issue on Github Submit Log in issue on Github
</Link> </Link>
</p> </p>

View File

@@ -439,7 +439,15 @@ export const composeRouter = createTRPCRouter({
} }
const projectName = slugify(`${project.name} ${input.id}`); const projectName = slugify(`${project.name} ${input.id}`);
const generate = processTemplate(template.config, { const appName = `${projectName}-${generatePassword(6)}`;
const config = {
...template.config,
variables: {
APP_NAME: appName,
...template.config.variables,
},
};
const generate = processTemplate(config, {
serverIp: serverIp, serverIp: serverIp,
projectName: projectName, projectName: projectName,
}); });
@@ -451,7 +459,7 @@ export const composeRouter = createTRPCRouter({
serverId: input.serverId, serverId: input.serverId,
name: input.id, name: input.id,
sourceType: "raw", sourceType: "raw",
appName: `${projectName}-${generatePassword(6)}`, appName: appName,
isolatedDeployment: true, isolatedDeployment: true,
}); });
@@ -605,7 +613,15 @@ export const composeRouter = createTRPCRouter({
}); });
} }
const processedTemplate = processTemplate(config, { const configModified = {
...config,
variables: {
APP_NAME: compose.appName,
...config.variables,
},
};
const processedTemplate = processTemplate(configModified, {
serverIp: serverIp, serverIp: serverIp,
projectName: compose.appName, projectName: compose.appName,
}); });
@@ -675,7 +691,15 @@ export const composeRouter = createTRPCRouter({
}); });
} }
const processedTemplate = processTemplate(config, { const configModified = {
...config,
variables: {
APP_NAME: compose.appName,
...config.variables,
},
};
const processedTemplate = processTemplate(configModified, {
serverIp: serverIp, serverIp: serverIp,
projectName: compose.appName, projectName: compose.appName,
}); });

View File

@@ -309,6 +309,7 @@ export const projectRouter = createTRPCRouter({
}), }),
) )
.optional(), .optional(),
duplicateInSameProject: z.boolean().default(false),
}), }),
) )
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
@@ -331,15 +332,17 @@ export const projectRouter = createTRPCRouter({
}); });
} }
// Create new project // Create new project or use existing one
const newProject = await createProject( const targetProject = input.duplicateInSameProject
{ ? sourceProject
name: input.name, : await createProject(
description: input.description, {
env: sourceProject.env, name: input.name,
}, description: input.description,
ctx.session.activeOrganizationId, env: sourceProject.env,
); },
ctx.session.activeOrganizationId,
);
if (input.includeServices) { if (input.includeServices) {
const servicesToDuplicate = input.selectedServices || []; const servicesToDuplicate = input.selectedServices || [];
@@ -362,7 +365,10 @@ export const projectRouter = createTRPCRouter({
const newApplication = await createApplication({ const newApplication = await createApplication({
...application, ...application,
projectId: newProject.projectId, name: input.duplicateInSameProject
? `${application.name} (copy)`
: application.name,
projectId: targetProject.projectId,
}); });
for (const domain of domains) { for (const domain of domains) {
@@ -423,7 +429,10 @@ export const projectRouter = createTRPCRouter({
const newPostgres = await createPostgres({ const newPostgres = await createPostgres({
...postgres, ...postgres,
projectId: newProject.projectId, name: input.duplicateInSameProject
? `${postgres.name} (copy)`
: postgres.name,
projectId: targetProject.projectId,
}); });
for (const mount of mounts) { for (const mount of mounts) {
@@ -449,7 +458,10 @@ export const projectRouter = createTRPCRouter({
await findMariadbById(id); await findMariadbById(id);
const newMariadb = await createMariadb({ const newMariadb = await createMariadb({
...mariadb, ...mariadb,
projectId: newProject.projectId, name: input.duplicateInSameProject
? `${mariadb.name} (copy)`
: mariadb.name,
projectId: targetProject.projectId,
}); });
for (const mount of mounts) { for (const mount of mounts) {
@@ -475,7 +487,10 @@ export const projectRouter = createTRPCRouter({
await findMongoById(id); await findMongoById(id);
const newMongo = await createMongo({ const newMongo = await createMongo({
...mongo, ...mongo,
projectId: newProject.projectId, name: input.duplicateInSameProject
? `${mongo.name} (copy)`
: mongo.name,
projectId: targetProject.projectId,
}); });
for (const mount of mounts) { for (const mount of mounts) {
@@ -501,7 +516,10 @@ export const projectRouter = createTRPCRouter({
await findMySqlById(id); await findMySqlById(id);
const newMysql = await createMysql({ const newMysql = await createMysql({
...mysql, ...mysql,
projectId: newProject.projectId, name: input.duplicateInSameProject
? `${mysql.name} (copy)`
: mysql.name,
projectId: targetProject.projectId,
}); });
for (const mount of mounts) { for (const mount of mounts) {
@@ -526,7 +544,10 @@ export const projectRouter = createTRPCRouter({
const { redisId, mounts, ...redis } = await findRedisById(id); const { redisId, mounts, ...redis } = await findRedisById(id);
const newRedis = await createRedis({ const newRedis = await createRedis({
...redis, ...redis,
projectId: newProject.projectId, name: input.duplicateInSameProject
? `${redis.name} (copy)`
: redis.name,
projectId: targetProject.projectId,
}); });
for (const mount of mounts) { for (const mount of mounts) {
@@ -545,7 +566,10 @@ export const projectRouter = createTRPCRouter({
await findComposeById(id); await findComposeById(id);
const newCompose = await createCompose({ const newCompose = await createCompose({
...compose, ...compose,
projectId: newProject.projectId, name: input.duplicateInSameProject
? `${compose.name} (copy)`
: compose.name,
projectId: targetProject.projectId,
}); });
for (const mount of mounts) { for (const mount of mounts) {
@@ -572,21 +596,20 @@ export const projectRouter = createTRPCRouter({
}; };
// Duplicate selected services // Duplicate selected services
for (const service of servicesToDuplicate) { for (const service of servicesToDuplicate) {
await duplicateService(service.id, service.type); await duplicateService(service.id, service.type);
} }
} }
if (ctx.user.role === "member") { if (!input.duplicateInSameProject && ctx.user.role === "member") {
await addNewProject( await addNewProject(
ctx.user.id, ctx.user.id,
newProject.projectId, targetProject.projectId,
ctx.session.activeOrganizationId, ctx.session.activeOrganizationId,
); );
} }
return newProject; return targetProject;
} catch (error) { } catch (error) {
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",

View File

@@ -356,20 +356,20 @@ const installUtilities = () => `
case "$OS_TYPE" in case "$OS_TYPE" in
arch) arch)
pacman -Sy --noconfirm --needed curl wget git jq openssl >/dev/null || true pacman -Sy --noconfirm --needed curl wget git git-lfs jq openssl >/dev/null || true
;; ;;
alpine) alpine)
sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories
apk update >/dev/null apk update >/dev/null
apk add curl wget git jq openssl sudo unzip tar >/dev/null apk add curl wget git git-lfs jq openssl sudo unzip tar >/dev/null
;; ;;
ubuntu | debian | raspbian) ubuntu | debian | raspbian)
DEBIAN_FRONTEND=noninteractive apt-get update -y >/dev/null DEBIAN_FRONTEND=noninteractive apt-get update -y >/dev/null
DEBIAN_FRONTEND=noninteractive apt-get install -y unzip curl wget git jq openssl >/dev/null DEBIAN_FRONTEND=noninteractive apt-get install -y unzip curl wget git git-lfs jq openssl >/dev/null
;; ;;
centos | fedora | rhel | ol | rocky | almalinux | amzn) centos | fedora | rhel | ol | rocky | almalinux | amzn)
if [ "$OS_TYPE" = "amzn" ]; then if [ "$OS_TYPE" = "amzn" ]; then
dnf install -y wget git jq openssl >/dev/null dnf install -y wget git git-lfs jq openssl >/dev/null
else else
if ! command -v dnf >/dev/null; then if ! command -v dnf >/dev/null; then
yum install -y dnf >/dev/null yum install -y dnf >/dev/null
@@ -377,12 +377,12 @@ const installUtilities = () => `
if ! command -v curl >/dev/null; then if ! command -v curl >/dev/null; then
dnf install -y curl >/dev/null dnf install -y curl >/dev/null
fi fi
dnf install -y wget git jq openssl unzip >/dev/null dnf install -y wget git git-lfs jq openssl unzip >/dev/null
fi fi
;; ;;
sles | opensuse-leap | opensuse-tumbleweed) sles | opensuse-leap | opensuse-tumbleweed)
zypper refresh >/dev/null zypper refresh >/dev/null
zypper install -y curl wget git jq openssl >/dev/null zypper install -y curl wget git git-lfs jq openssl >/dev/null
;; ;;
*) *)
echo "This script only supports Debian, Redhat, Arch Linux, or SLES based operating systems for now." echo "This script only supports Debian, Redhat, Arch Linux, or SLES based operating systems for now."
@@ -577,7 +577,7 @@ const installNixpacks = () => `
if command_exists nixpacks; then if command_exists nixpacks; then
echo "Nixpacks already installed ✅" echo "Nixpacks already installed ✅"
else else
export NIXPACKS_VERSION=1.35.0 export NIXPACKS_VERSION=1.39.0
bash -c "$(curl -fsSL https://nixpacks.com/install.sh)" bash -c "$(curl -fsSL https://nixpacks.com/install.sh)"
echo "Nixpacks version $NIXPACKS_VERSION installed ✅" echo "Nixpacks version $NIXPACKS_VERSION installed ✅"
fi fi

View File

@@ -3,7 +3,7 @@ import { execAsync } from "../process/execAsync";
import { getS3Credentials, normalizeS3Path } 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, rm } from "node:fs/promises";
import { join } from "node:path"; import { join } from "node:path";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { import {
@@ -51,10 +51,20 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
const postgresContainerId = containerId.trim(); const postgresContainerId = containerId.trim();
const postgresCommand = `docker exec ${postgresContainerId} pg_dump -v -Fc -U dokploy -d dokploy > '${tempDir}/database.sql'`; // First dump the database inside the container
const dumpCommand = `docker exec ${postgresContainerId} pg_dump -v -Fc -U dokploy -d dokploy -f /tmp/database.sql`;
writeStream.write(`Running dump command: ${dumpCommand}\n`);
await execAsync(dumpCommand);
writeStream.write(`Running command: ${postgresCommand}\n`); // Then copy the file from the container to host
await execAsync(postgresCommand); const copyCommand = `docker cp ${postgresContainerId}:/tmp/database.sql ${tempDir}/database.sql`;
writeStream.write(`Copying database dump: ${copyCommand}\n`);
await execAsync(copyCommand);
// Clean up the temp file in the container
const cleanupCommand = `docker exec ${postgresContainerId} rm -f /tmp/database.sql`;
writeStream.write(`Cleaning up temp file: ${cleanupCommand}\n`);
await execAsync(cleanupCommand);
await execAsync( await execAsync(
`rsync -av --ignore-errors ${BASE_PATH}/ ${tempDir}/filesystem/`, `rsync -av --ignore-errors ${BASE_PATH}/ ${tempDir}/filesystem/`,
@@ -77,7 +87,11 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
await updateDeploymentStatus(deployment.deploymentId, "done"); await updateDeploymentStatus(deployment.deploymentId, "done");
return true; return true;
} finally { } finally {
await execAsync(`rm -rf ${tempDir}`); try {
await rm(tempDir, { recursive: true, force: true });
} catch (cleanupError) {
console.error("Cleanup error:", cleanupError);
}
} }
} catch (error) { } catch (error) {
console.error("Backup error:", error); console.error("Backup error:", error);

View File

@@ -190,7 +190,8 @@ const createEnvFile = (compose: ComposeNested) => {
join(COMPOSE_PATH, appName, "code", "docker-compose.yml"); join(COMPOSE_PATH, appName, "code", "docker-compose.yml");
const envFilePath = join(dirname(composeFilePath), ".env"); const envFilePath = join(dirname(composeFilePath), ".env");
let envContent = env || ""; let envContent = `APP_NAME=${appName}\n`;
envContent += env || "";
if (!envContent.includes("DOCKER_CONFIG")) { if (!envContent.includes("DOCKER_CONFIG")) {
envContent += "\nDOCKER_CONFIG=/root/.docker/config.json"; envContent += "\nDOCKER_CONFIG=/root/.docker/config.json";
} }
@@ -219,7 +220,8 @@ export const getCreateEnvFileCommand = (compose: ComposeNested) => {
const envFilePath = join(dirname(composeFilePath), ".env"); const envFilePath = join(dirname(composeFilePath), ".env");
let envContent = env || ""; let envContent = `APP_NAME=${appName}\n`;
envContent += env || "";
if (!envContent.includes("DOCKER_CONFIG")) { if (!envContent.includes("DOCKER_CONFIG")) {
envContent += "\nDOCKER_CONFIG=/root/.docker/config.json"; envContent += "\nDOCKER_CONFIG=/root/.docker/config.json";
} }

View File

@@ -246,32 +246,16 @@ export const getGitlabRepositories = async (gitlabId?: string) => {
const gitlabProvider = await findGitlabById(gitlabId); const gitlabProvider = await findGitlabById(gitlabId);
const response = await fetch( const allProjects = await validateGitlabProvider(gitlabProvider);
`${gitlabProvider.gitlabUrl}/api/v4/projects?membership=true&owned=true&page=${0}&per_page=${100}`,
{
headers: {
Authorization: `Bearer ${gitlabProvider.accessToken}`,
},
},
);
if (!response.ok) { const filteredRepos = allProjects.filter((repo: any) => {
throw new TRPCError({
code: "BAD_REQUEST",
message: `Failed to fetch repositories: ${response.statusText}`,
});
}
const repositories = await response.json();
const filteredRepos = repositories.filter((repo: any) => {
const { full_path, kind } = repo.namespace; const { full_path, kind } = repo.namespace;
const groupName = gitlabProvider.groupName?.toLowerCase(); const groupName = gitlabProvider.groupName?.toLowerCase();
if (groupName) { if (groupName) {
const isIncluded = groupName const isIncluded = groupName
.split(",") .split(",")
.some((name) => full_path.toLowerCase().includes(name)); .some((name) => full_path === name);
return isIncluded && kind === "group"; return isIncluded && kind === "group";
} }
@@ -432,34 +416,60 @@ export const testGitlabConnection = async (
const gitlabProvider = await findGitlabById(gitlabId); const gitlabProvider = await findGitlabById(gitlabId);
const response = await fetch( const repositories = await validateGitlabProvider(gitlabProvider);
`${gitlabProvider.gitlabUrl}/api/v4/projects?membership=true&owned=true&page=${0}&per_page=${100}`,
{
headers: {
Authorization: `Bearer ${gitlabProvider.accessToken}`,
},
},
);
if (!response.ok) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `Failed to fetch repositories: ${response.statusText}`,
});
}
const repositories = await response.json();
const filteredRepos = repositories.filter((repo: any) => { const filteredRepos = repositories.filter((repo: any) => {
const { full_path, kind } = repo.namespace; const { full_path, kind } = repo.namespace;
if (groupName) { if (groupName) {
return groupName return groupName.split(",").some((name) => full_path === name);
.split(",")
.some((name) => full_path.toLowerCase().includes(name));
} }
return kind === "user"; return kind === "user";
}); });
return filteredRepos.length; return filteredRepos.length;
}; };
export const validateGitlabProvider = async (gitlabProvider: Gitlab) => {
try {
const allProjects = [];
let page = 1;
const perPage = 100; // GitLab's max per page is 100
while (true) {
const response = await fetch(
`${gitlabProvider.gitlabUrl}/api/v4/projects?membership=true&owned=true&page=${page}&per_page=${perPage}`,
{
headers: {
Authorization: `Bearer ${gitlabProvider.accessToken}`,
},
},
);
if (!response.ok) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `Failed to fetch repositories: ${response.statusText}`,
});
}
const projects = await response.json();
if (projects.length === 0) {
break;
}
allProjects.push(...projects);
page++;
const total = response.headers.get("x-total");
if (total && allProjects.length >= Number.parseInt(total)) {
break;
}
}
return allProjects;
} catch (error) {
throw error;
}
};