From 36ba9fcb3e8152b067cdc178f3993fdaef793a21 Mon Sep 17 00:00:00 2001 From: Laode Muhammad Al Fatih Date: Fri, 10 Apr 2026 16:18:37 +0700 Subject: [PATCH] feat: add "Deploy with Fresh Volumes" button for Docker Compose projects Adds a one-click option to redeploy Docker Compose services with clean volumes, removing the need to manually edit the run command. When triggered, it runs `docker compose down --volumes` before the standard deploy build step. The feature is only available for docker-compose type projects (not swarm stacks). --- .../dashboard/compose/general/actions.tsx | 51 ++++++++++++++++++- apps/dokploy/server/api/routers/compose.ts | 2 + .../server/queues/deployments-queue.ts | 2 + apps/dokploy/server/queues/queue-types.ts | 1 + packages/server/src/db/schema/compose.ts | 2 + packages/server/src/services/compose.ts | 24 +++++++++ 6 files changed, 81 insertions(+), 1 deletion(-) diff --git a/apps/dokploy/components/dashboard/compose/general/actions.tsx b/apps/dokploy/components/dashboard/compose/general/actions.tsx index d04725e26..71c703315 100644 --- a/apps/dokploy/components/dashboard/compose/general/actions.tsx +++ b/apps/dokploy/components/dashboard/compose/general/actions.tsx @@ -1,5 +1,5 @@ import * as TooltipPrimitive from "@radix-ui/react-tooltip"; -import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react"; +import { Ban, CheckCircle2, HardDriveDownload, RefreshCcw, Rocket, Terminal } from "lucide-react"; import { useRouter } from "next/router"; import { toast } from "sonner"; import { DialogAction } from "@/components/shared/dialog-action"; @@ -82,6 +82,55 @@ export const ComposeActions = ({ composeId }: Props) => { )} + {canDeploy && + data?.composeType === "docker-compose" && ( + { + await deploy({ + composeId: composeId, + freshVolumes: true, + }) + .then(() => { + toast.success( + "Compose deployed with fresh volumes", + ); + refetch(); + router.push( + `/dashboard/project/${data?.environment.projectId}/environment/${data?.environmentId}/services/compose/${composeId}?tab=deployments`, + ); + }) + .catch(() => { + toast.error("Error deploying compose"); + }); + }} + > + + + )} {canDeploy && ( composeId: job.data.composeId, titleLog: job.data.titleLog, descriptionLog: job.data.descriptionLog, + freshVolumes: job.data.freshVolumes, }); } else if (job.data.type === "redeploy") { await rebuildCompose({ composeId: job.data.composeId, titleLog: job.data.titleLog, descriptionLog: job.data.descriptionLog, + freshVolumes: job.data.freshVolumes, }); } } else if (job.data.applicationType === "application-preview") { diff --git a/apps/dokploy/server/queues/queue-types.ts b/apps/dokploy/server/queues/queue-types.ts index 1000725ad..5c469dd9e 100644 --- a/apps/dokploy/server/queues/queue-types.ts +++ b/apps/dokploy/server/queues/queue-types.ts @@ -16,6 +16,7 @@ type DeployJob = type: "deploy" | "redeploy"; applicationType: "compose"; serverId?: string; + freshVolumes?: boolean; } | { applicationId: string; diff --git a/packages/server/src/db/schema/compose.ts b/packages/server/src/db/schema/compose.ts index 7803cb0a7..0416e5ea3 100644 --- a/packages/server/src/db/schema/compose.ts +++ b/packages/server/src/db/schema/compose.ts @@ -198,12 +198,14 @@ export const apiDeployCompose = z.object({ composeId: z.string().min(1), title: z.string().optional(), description: z.string().optional(), + freshVolumes: z.boolean().optional(), }); export const apiRedeployCompose = z.object({ composeId: z.string().min(1), title: z.string().optional(), description: z.string().optional(), + freshVolumes: z.boolean().optional(), }); export const apiDeleteCompose = z.object({ diff --git a/packages/server/src/services/compose.ts b/packages/server/src/services/compose.ts index 0cec3418b..d78e63a41 100644 --- a/packages/server/src/services/compose.ts +++ b/packages/server/src/services/compose.ts @@ -209,10 +209,12 @@ export const deployCompose = async ({ composeId, titleLog = "Manual deployment", descriptionLog = "", + freshVolumes = false, }: { composeId: string; titleLog: string; descriptionLog: string; + freshVolumes?: boolean; }) => { const compose = await findComposeById(composeId); @@ -266,6 +268,16 @@ export const deployCompose = async ({ } } + if (freshVolumes && compose.composeType === "docker-compose") { + const downCommand = `set -e; docker compose -p ${compose.appName} down --volumes 2>&1 || true;`; + const downWithLog = `(${downCommand}) >> ${deployment.logPath} 2>&1`; + if (compose.serverId) { + await execAsyncRemote(compose.serverId, downWithLog); + } else { + await execAsync(downWithLog); + } + } + command = "set -e;"; command += await getBuildComposeCommand(entity); commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`; @@ -339,10 +351,12 @@ export const rebuildCompose = async ({ composeId, titleLog = "Rebuild deployment", descriptionLog = "", + freshVolumes = false, }: { composeId: string; titleLog: string; descriptionLog: string; + freshVolumes?: boolean; }) => { const compose = await findComposeById(composeId); @@ -380,6 +394,16 @@ export const rebuildCompose = async ({ } } + if (freshVolumes && compose.composeType === "docker-compose") { + const downCommand = `set -e; docker compose -p ${compose.appName} down --volumes 2>&1 || true;`; + const downWithLog = `(${downCommand}) >> ${deployment.logPath} 2>&1`; + if (compose.serverId) { + await execAsyncRemote(compose.serverId, downWithLog); + } else { + await execAsync(downWithLog); + } + } + command = "set -e;"; command += await getBuildComposeCommand(compose); commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;