diff --git a/Dockerfile b/Dockerfile index 98ed9851..c2a9fd89 100644 --- a/Dockerfile +++ b/Dockerfile @@ -49,7 +49,7 @@ RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm # Install Nixpacks and tsx # | 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 \ && chmod +x install.sh \ && ./install.sh \ @@ -63,4 +63,4 @@ RUN curl -sSL https://railpack.com/install.sh | bash COPY --from=buildpacksio/pack:0.35.0 /usr/local/bin/pack /usr/local/bin/pack EXPOSE 3000 -CMD [ "pnpm", "start" ] \ No newline at end of file +CMD [ "pnpm", "start" ] diff --git a/apps/dokploy/Dockerfile b/apps/dokploy/Dockerfile index f4188c54..0537b03e 100644 --- a/apps/dokploy/Dockerfile +++ b/apps/dokploy/Dockerfile @@ -8,7 +8,7 @@ COPY . /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 RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile diff --git a/apps/dokploy/components/dashboard/application/advanced/import/show-import.tsx b/apps/dokploy/components/dashboard/application/advanced/import/show-import.tsx index 0e848fec..aa359d67 100644 --- a/apps/dokploy/components/dashboard/application/advanced/import/show-import.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/import/show-import.tsx @@ -263,7 +263,7 @@ export const ShowImport = ({ composeId }: Props) => { {templateInfo.template.envs.map((env, index) => (
{env}
@@ -328,7 +328,7 @@ export const ShowImport = ({ composeId }: Props) => { Mount File Content - + Webhook URL:
- {`${url}/api/deploy/${refreshToken}`} + {`${url}/api/deploy${type === "compose" ? "/compose" : ""}/${refreshToken}`} {(type === "application" || type === "compose") && ( diff --git a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx index 26df7b69..06fa5743 100644 --- a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx +++ b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx @@ -186,30 +186,19 @@ export const ShowDomains = ({ id, type }: Props) => { return (
{/* Service & Domain Info */} -
-
- {item.serviceName && ( - - - {item.serviceName} - - )} - - - {item.host} - - -
-
+
+ {item.serviceName && ( + + + {item.serviceName} + + )} +
{!item.host.includes("traefik.me") && ( {
+
+ + {item.host} + + +
{/* Domain Details */}
diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-bitbucket-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-bitbucket-provider.tsx index f0179d9c..befc8595 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-bitbucket-provider.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-bitbucket-provider.tsx @@ -136,7 +136,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => { enableSubmodules: data.enableSubmodules || false, }); } - }, [form.reset, data, form]); + }, [form.reset, data?.applicationId, form]); const onSubmit = async (data: BitbucketProvider) => { await mutateAsync({ @@ -435,7 +435,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
{ if (e.key === "Enter") { e.preventDefault(); @@ -454,7 +454,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => { variant="secondary" onClick={() => { const input = document.querySelector( - 'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]', + 'input[placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"]', ) as HTMLInputElement; const value = input.value.trim(); if (value) { diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-docker-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-docker-provider.tsx index a1f3367d..72b2578c 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-docker-provider.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-docker-provider.tsx @@ -53,7 +53,7 @@ export const SaveDockerProvider = ({ applicationId }: Props) => { registryURL: data.registryUrl || "", }); } - }, [form.reset, data, form]); + }, [form.reset, data?.applicationId, form]); const onSubmit = async (values: DockerProvider) => { await mutateAsync({ diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-git-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-git-provider.tsx index 25858207..68f56055 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-git-provider.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-git-provider.tsx @@ -262,7 +262,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
{ if (e.key === "Enter") { e.preventDefault(); @@ -281,7 +281,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => { variant="secondary" onClick={() => { const input = document.querySelector( - 'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]', + 'input[placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"]', ) as HTMLInputElement; const value = input.value.trim(); if (value) { diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-gitea-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-gitea-provider.tsx index 531ace12..55fbfebd 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-gitea-provider.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-gitea-provider.tsx @@ -158,7 +158,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => { enableSubmodules: data.enableSubmodules || false, }); } - }, [form.reset, data, form]); + }, [form.reset, data?.applicationId, form]); const onSubmit = async (data: GiteaProvider) => { await mutateAsync({ @@ -470,7 +470,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
{ if (e.key === "Enter") { e.preventDefault(); diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx index 0bf1ac8a..c76b9ae5 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx @@ -134,7 +134,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => { enableSubmodules: data.enableSubmodules ?? false, }); } - }, [form.reset, data, form]); + }, [form.reset, data?.applicationId, form]); const onSubmit = async (data: GithubProvider) => { await mutateAsync({ @@ -474,7 +474,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
{ if (e.key === "Enter") { e.preventDefault(); diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx index b4b55d3f..b4f27da7 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx @@ -141,7 +141,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => { enableSubmodules: data.enableSubmodules ?? false, }); } - }, [form.reset, data, form]); + }, [form.reset, data?.applicationId, form]); const onSubmit = async (data: GitlabProvider) => { await mutateAsync({ @@ -452,7 +452,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
{ if (e.key === "Enter") { e.preventDefault(); diff --git a/apps/dokploy/components/dashboard/compose/general/generic/save-bitbucket-provider-compose.tsx b/apps/dokploy/components/dashboard/compose/general/generic/save-bitbucket-provider-compose.tsx index 353ccc6c..73d8cf1c 100644 --- a/apps/dokploy/components/dashboard/compose/general/generic/save-bitbucket-provider-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/general/generic/save-bitbucket-provider-compose.tsx @@ -136,7 +136,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => { enableSubmodules: data.enableSubmodules ?? false, }); } - }, [form.reset, data, form]); + }, [form.reset, data?.composeId, form]); const onSubmit = async (data: BitbucketProvider) => { await mutateAsync({ @@ -437,7 +437,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
{ if (e.key === "Enter") { e.preventDefault(); @@ -456,7 +456,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => { variant="secondary" onClick={() => { const input = document.querySelector( - 'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]', + 'input[placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"]', ) as HTMLInputElement; const value = input.value.trim(); if (value) { diff --git a/apps/dokploy/components/dashboard/compose/general/generic/save-git-provider-compose.tsx b/apps/dokploy/components/dashboard/compose/general/generic/save-git-provider-compose.tsx index a5968e02..fc90f4f1 100644 --- a/apps/dokploy/components/dashboard/compose/general/generic/save-git-provider-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/general/generic/save-git-provider-compose.tsx @@ -263,7 +263,7 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
{ if (e.key === "Enter") { e.preventDefault(); @@ -282,7 +282,7 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => { variant="secondary" onClick={() => { const input = document.querySelector( - 'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]', + 'input[placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"]', ) as HTMLInputElement; const value = input.value.trim(); if (value) { diff --git a/apps/dokploy/components/dashboard/compose/general/generic/save-gitea-provider-compose.tsx b/apps/dokploy/components/dashboard/compose/general/generic/save-gitea-provider-compose.tsx index 6f9b50da..0b57b03d 100644 --- a/apps/dokploy/components/dashboard/compose/general/generic/save-gitea-provider-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/general/generic/save-gitea-provider-compose.tsx @@ -142,7 +142,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => { enableSubmodules: data.enableSubmodules ?? false, }); } - }, [form.reset, data, form]); + }, [form.reset, data?.composeId, form]); const onSubmit = async (data: GiteaProvider) => { await mutateAsync({ @@ -437,7 +437,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
{ if (e.key === "Enter") { e.preventDefault(); diff --git a/apps/dokploy/components/dashboard/compose/general/generic/save-github-provider-compose.tsx b/apps/dokploy/components/dashboard/compose/general/generic/save-github-provider-compose.tsx index 97b57f0b..5b2019fe 100644 --- a/apps/dokploy/components/dashboard/compose/general/generic/save-github-provider-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/general/generic/save-github-provider-compose.tsx @@ -134,7 +134,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => { enableSubmodules: data.enableSubmodules ?? false, }); } - }, [form.reset, data, form]); + }, [form.reset, data?.composeId, form]); const onSubmit = async (data: GithubProvider) => { await mutateAsync({ @@ -474,7 +474,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
{ if (e.key === "Enter") { e.preventDefault(); @@ -496,7 +496,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => { variant="secondary" onClick={() => { const input = document.querySelector( - 'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]', + 'input[placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"]', ) as HTMLInputElement; const value = input.value.trim(); if (value) { diff --git a/apps/dokploy/components/dashboard/compose/general/generic/save-gitlab-provider-compose.tsx b/apps/dokploy/components/dashboard/compose/general/generic/save-gitlab-provider-compose.tsx index 30b542ce..c630cd71 100644 --- a/apps/dokploy/components/dashboard/compose/general/generic/save-gitlab-provider-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/general/generic/save-gitlab-provider-compose.tsx @@ -142,7 +142,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => { enableSubmodules: data.enableSubmodules ?? false, }); } - }, [form.reset, data, form]); + }, [form.reset, data?.composeId, form]); const onSubmit = async (data: GitlabProvider) => { await mutateAsync({ @@ -453,7 +453,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
{ if (e.key === "Enter") { e.preventDefault(); @@ -472,7 +472,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => { variant="secondary" onClick={() => { const input = document.querySelector( - 'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]', + 'input[placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"]', ) as HTMLInputElement; const value = input.value.trim(); if (value) { diff --git a/apps/dokploy/components/dashboard/compose/general/show-converted-compose.tsx b/apps/dokploy/components/dashboard/compose/general/show-converted-compose.tsx index 89a9e075..77f331bd 100644 --- a/apps/dokploy/components/dashboard/compose/general/show-converted-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/general/show-converted-compose.tsx @@ -10,7 +10,7 @@ import { DialogTrigger, } from "@/components/ui/dialog"; import { api } from "@/utils/api"; -import { Puzzle, RefreshCw } from "lucide-react"; +import { Loader2, Puzzle, RefreshCw } from "lucide-react"; import { useEffect, useState } from "react"; import { toast } from "sonner"; @@ -66,36 +66,50 @@ export const ShowConvertedCompose = ({ composeId }: Props) => { Preview your docker-compose file with added domains. Note: At least one domain must be specified for this conversion to take effect. + {isLoading ? ( +
+ +
+ ) : compose?.length === 5 ? ( +
+ + + No converted compose data available. + +
+ ) : ( + <> +
+ +
-
- -
- -
-					
-				
+
+							
+						
+ + )} ); diff --git a/apps/dokploy/components/dashboard/project/duplicate-project.tsx b/apps/dokploy/components/dashboard/project/duplicate-project.tsx index 038ddcb6..d8abea2d 100644 --- a/apps/dokploy/components/dashboard/project/duplicate-project.tsx +++ b/apps/dokploy/components/dashboard/project/duplicate-project.tsx @@ -15,6 +15,7 @@ import { Copy, Loader2 } from "lucide-react"; import { useRouter } from "next/router"; import { useState } from "react"; import { toast } from "sonner"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; export type Services = { appName: string; @@ -48,6 +49,7 @@ export const DuplicateProject = ({ const [open, setOpen] = useState(false); const [name, setName] = useState(""); const [description, setDescription] = useState(""); + const [duplicateType, setDuplicateType] = useState("new-project"); // "new-project" or "same-project" const utils = api.useUtils(); const router = useRouter(); @@ -59,9 +61,15 @@ export const DuplicateProject = ({ api.project.duplicate.useMutation({ onSuccess: async (newProject) => { await utils.project.all.invalidate(); - toast.success("Project duplicated successfully"); + toast.success( + duplicateType === "new-project" + ? "Project duplicated successfully" + : "Services duplicated successfully", + ); setOpen(false); - router.push(`/dashboard/project/${newProject.projectId}`); + if (duplicateType === "new-project") { + router.push(`/dashboard/project/${newProject.projectId}`); + } }, onError: (error) => { toast.error(error.message); @@ -69,7 +77,7 @@ export const DuplicateProject = ({ }); const handleDuplicate = async () => { - if (!name) { + if (duplicateType === "new-project" && !name) { toast.error("Project name is required"); return; } @@ -83,6 +91,7 @@ export const DuplicateProject = ({ id: service.id, type: service.type, })), + duplicateInSameProject: duplicateType === "same-project", }); }; @@ -95,6 +104,7 @@ export const DuplicateProject = ({ // Reset form when closing setName(""); setDescription(""); + setDuplicateType("new-project"); } }} > @@ -106,32 +116,54 @@ export const DuplicateProject = ({ - Duplicate Project + Duplicate Services - Create a new project with the selected services + Choose where to duplicate the selected services
- - setName(e.target.value)} - placeholder="New project name" - /> + + +
+ + +
+
+ + +
+
-
- - setDescription(e.target.value)} - placeholder="Project description (optional)" - /> -
+ {duplicateType === "new-project" && ( + <> +
+ + setName(e.target.value)} + placeholder="New project name" + /> +
+ +
+ + setDescription(e.target.value)} + placeholder="Project description (optional)" + /> +
+ + )}
@@ -159,10 +191,14 @@ export const DuplicateProject = ({ {isLoading ? ( <> - Duplicating... + {duplicateType === "new-project" + ? "Duplicating project..." + : "Duplicating services..."} + ) : duplicateType === "new-project" ? ( + "Duplicate project" ) : ( - "Duplicate" + "Duplicate services" )} diff --git a/apps/dokploy/components/dashboard/settings/servers/handle-servers.tsx b/apps/dokploy/components/dashboard/settings/servers/handle-servers.tsx index 97994145..a2c9b50a 100644 --- a/apps/dokploy/components/dashboard/settings/servers/handle-servers.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/handle-servers.tsx @@ -156,6 +156,67 @@ export const HandleServers = ({ serverId }: Props) => { remotely. +
+

+ 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. +

+ + + You are free to use whatever provider, but we recommend to use one + of the above, to avoid issues. + +
{!canCreateMoreServers && ( You cannot create more servers,{" "} diff --git a/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/welcome-suscription.tsx b/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/welcome-suscription.tsx index bab93047..1ec4f2ab 100644 --- a/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/welcome-suscription.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/welcome-suscription.tsx @@ -177,6 +177,14 @@ export const WelcomeSuscription = () => { Hostinger - Get 20% Discount +
  • + + American Cloud - Get $20 Credits + +
  • - Submit Log in issue on Github + + Submit Log in issue on Github +

    diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx index 39f399e9..80909563 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx @@ -217,12 +217,12 @@ const Service = (
    General diff --git a/apps/dokploy/server/api/routers/compose.ts b/apps/dokploy/server/api/routers/compose.ts index 48c2bfa7..583dde5c 100644 --- a/apps/dokploy/server/api/routers/compose.ts +++ b/apps/dokploy/server/api/routers/compose.ts @@ -439,7 +439,15 @@ export const composeRouter = createTRPCRouter({ } 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, projectName: projectName, }); @@ -451,7 +459,7 @@ export const composeRouter = createTRPCRouter({ serverId: input.serverId, name: input.id, sourceType: "raw", - appName: `${projectName}-${generatePassword(6)}`, + appName: appName, 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, 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, projectName: compose.appName, }); diff --git a/apps/dokploy/server/api/routers/project.ts b/apps/dokploy/server/api/routers/project.ts index 05875450..b98c93fa 100644 --- a/apps/dokploy/server/api/routers/project.ts +++ b/apps/dokploy/server/api/routers/project.ts @@ -309,6 +309,7 @@ export const projectRouter = createTRPCRouter({ }), ) .optional(), + duplicateInSameProject: z.boolean().default(false), }), ) .mutation(async ({ ctx, input }) => { @@ -331,15 +332,17 @@ export const projectRouter = createTRPCRouter({ }); } - // Create new project - const newProject = await createProject( - { - name: input.name, - description: input.description, - env: sourceProject.env, - }, - ctx.session.activeOrganizationId, - ); + // Create new project or use existing one + const targetProject = input.duplicateInSameProject + ? sourceProject + : await createProject( + { + name: input.name, + description: input.description, + env: sourceProject.env, + }, + ctx.session.activeOrganizationId, + ); if (input.includeServices) { const servicesToDuplicate = input.selectedServices || []; @@ -362,7 +365,10 @@ export const projectRouter = createTRPCRouter({ const newApplication = await createApplication({ ...application, - projectId: newProject.projectId, + name: input.duplicateInSameProject + ? `${application.name} (copy)` + : application.name, + projectId: targetProject.projectId, }); for (const domain of domains) { @@ -423,7 +429,10 @@ export const projectRouter = createTRPCRouter({ const newPostgres = await createPostgres({ ...postgres, - projectId: newProject.projectId, + name: input.duplicateInSameProject + ? `${postgres.name} (copy)` + : postgres.name, + projectId: targetProject.projectId, }); for (const mount of mounts) { @@ -449,7 +458,10 @@ export const projectRouter = createTRPCRouter({ await findMariadbById(id); const newMariadb = await createMariadb({ ...mariadb, - projectId: newProject.projectId, + name: input.duplicateInSameProject + ? `${mariadb.name} (copy)` + : mariadb.name, + projectId: targetProject.projectId, }); for (const mount of mounts) { @@ -475,7 +487,10 @@ export const projectRouter = createTRPCRouter({ await findMongoById(id); const newMongo = await createMongo({ ...mongo, - projectId: newProject.projectId, + name: input.duplicateInSameProject + ? `${mongo.name} (copy)` + : mongo.name, + projectId: targetProject.projectId, }); for (const mount of mounts) { @@ -501,7 +516,10 @@ export const projectRouter = createTRPCRouter({ await findMySqlById(id); const newMysql = await createMysql({ ...mysql, - projectId: newProject.projectId, + name: input.duplicateInSameProject + ? `${mysql.name} (copy)` + : mysql.name, + projectId: targetProject.projectId, }); for (const mount of mounts) { @@ -526,7 +544,10 @@ export const projectRouter = createTRPCRouter({ const { redisId, mounts, ...redis } = await findRedisById(id); const newRedis = await createRedis({ ...redis, - projectId: newProject.projectId, + name: input.duplicateInSameProject + ? `${redis.name} (copy)` + : redis.name, + projectId: targetProject.projectId, }); for (const mount of mounts) { @@ -545,7 +566,10 @@ export const projectRouter = createTRPCRouter({ await findComposeById(id); const newCompose = await createCompose({ ...compose, - projectId: newProject.projectId, + name: input.duplicateInSameProject + ? `${compose.name} (copy)` + : compose.name, + projectId: targetProject.projectId, }); for (const mount of mounts) { @@ -572,21 +596,20 @@ export const projectRouter = createTRPCRouter({ }; // Duplicate selected services - for (const service of servicesToDuplicate) { await duplicateService(service.id, service.type); } } - if (ctx.user.role === "member") { + if (!input.duplicateInSameProject && ctx.user.role === "member") { await addNewProject( ctx.user.id, - newProject.projectId, + targetProject.projectId, ctx.session.activeOrganizationId, ); } - return newProject; + return targetProject; } catch (error) { throw new TRPCError({ code: "BAD_REQUEST", diff --git a/packages/server/src/setup/server-setup.ts b/packages/server/src/setup/server-setup.ts index d1d42257..d6d60c2e 100644 --- a/packages/server/src/setup/server-setup.ts +++ b/packages/server/src/setup/server-setup.ts @@ -356,20 +356,20 @@ const installUtilities = () => ` case "$OS_TYPE" in 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) sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories 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) 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) 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 if ! command -v dnf >/dev/null; then yum install -y dnf >/dev/null @@ -377,12 +377,12 @@ const installUtilities = () => ` if ! command -v curl >/dev/null; then dnf install -y curl >/dev/null fi - dnf install -y wget git jq openssl unzip >/dev/null + dnf install -y wget git git-lfs jq openssl unzip >/dev/null fi ;; sles | opensuse-leap | opensuse-tumbleweed) 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." @@ -577,7 +577,7 @@ const installNixpacks = () => ` if command_exists nixpacks; then echo "Nixpacks already installed ✅" else - export NIXPACKS_VERSION=1.35.0 + export NIXPACKS_VERSION=1.39.0 bash -c "$(curl -fsSL https://nixpacks.com/install.sh)" echo "Nixpacks version $NIXPACKS_VERSION installed ✅" fi diff --git a/packages/server/src/utils/backups/web-server.ts b/packages/server/src/utils/backups/web-server.ts index 71df47ba..ed6b020f 100644 --- a/packages/server/src/utils/backups/web-server.ts +++ b/packages/server/src/utils/backups/web-server.ts @@ -3,7 +3,7 @@ import { execAsync } from "../process/execAsync"; import { getS3Credentials, normalizeS3Path } from "./utils"; import { findDestinationById } from "@dokploy/server/services/destination"; 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 { tmpdir } from "node:os"; import { @@ -51,10 +51,20 @@ export const runWebServerBackup = async (backup: BackupSchedule) => { 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`); - await execAsync(postgresCommand); + // Then copy the file from the container to host + 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( `rsync -av --ignore-errors ${BASE_PATH}/ ${tempDir}/filesystem/`, @@ -77,7 +87,11 @@ export const runWebServerBackup = async (backup: BackupSchedule) => { await updateDeploymentStatus(deployment.deploymentId, "done"); return true; } finally { - await execAsync(`rm -rf ${tempDir}`); + try { + await rm(tempDir, { recursive: true, force: true }); + } catch (cleanupError) { + console.error("Cleanup error:", cleanupError); + } } } catch (error) { console.error("Backup error:", error); diff --git a/packages/server/src/utils/builders/compose.ts b/packages/server/src/utils/builders/compose.ts index 7b00fc72..92add1e6 100644 --- a/packages/server/src/utils/builders/compose.ts +++ b/packages/server/src/utils/builders/compose.ts @@ -190,7 +190,8 @@ const createEnvFile = (compose: ComposeNested) => { join(COMPOSE_PATH, appName, "code", "docker-compose.yml"); const envFilePath = join(dirname(composeFilePath), ".env"); - let envContent = env || ""; + let envContent = `APP_NAME=${appName}\n`; + envContent += env || ""; if (!envContent.includes("DOCKER_CONFIG")) { envContent += "\nDOCKER_CONFIG=/root/.docker/config.json"; } @@ -219,7 +220,8 @@ export const getCreateEnvFileCommand = (compose: ComposeNested) => { const envFilePath = join(dirname(composeFilePath), ".env"); - let envContent = env || ""; + let envContent = `APP_NAME=${appName}\n`; + envContent += env || ""; if (!envContent.includes("DOCKER_CONFIG")) { envContent += "\nDOCKER_CONFIG=/root/.docker/config.json"; } diff --git a/packages/server/src/utils/providers/gitlab.ts b/packages/server/src/utils/providers/gitlab.ts index 9e848fd5..7fa804eb 100644 --- a/packages/server/src/utils/providers/gitlab.ts +++ b/packages/server/src/utils/providers/gitlab.ts @@ -246,32 +246,16 @@ export const getGitlabRepositories = async (gitlabId?: string) => { const gitlabProvider = await findGitlabById(gitlabId); - const response = await fetch( - `${gitlabProvider.gitlabUrl}/api/v4/projects?membership=true&owned=true&page=${0}&per_page=${100}`, - { - headers: { - Authorization: `Bearer ${gitlabProvider.accessToken}`, - }, - }, - ); + const allProjects = await validateGitlabProvider(gitlabProvider); - 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 = allProjects.filter((repo: any) => { const { full_path, kind } = repo.namespace; const groupName = gitlabProvider.groupName?.toLowerCase(); if (groupName) { const isIncluded = groupName .split(",") - .some((name) => full_path.toLowerCase().includes(name)); + .some((name) => full_path === name); return isIncluded && kind === "group"; } @@ -432,34 +416,60 @@ export const testGitlabConnection = async ( const gitlabProvider = await findGitlabById(gitlabId); - const response = await fetch( - `${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 repositories = await validateGitlabProvider(gitlabProvider); const filteredRepos = repositories.filter((repo: any) => { const { full_path, kind } = repo.namespace; if (groupName) { - return groupName - .split(",") - .some((name) => full_path.toLowerCase().includes(name)); + return groupName.split(",").some((name) => full_path === name); } return kind === "user"; }); 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; + } +};