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).
This commit is contained in:
Laode Muhammad Al Fatih
2026-04-10 16:18:37 +07:00
parent b4c57b6326
commit 36ba9fcb3e
6 changed files with 81 additions and 1 deletions
@@ -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) => {
</Button>
</DialogAction>
)}
{canDeploy &&
data?.composeType === "docker-compose" && (
<DialogAction
title="Deploy with Fresh Volumes"
description="This will remove all volumes and redeploy with a clean state. All persistent data will be permanently deleted."
type="destructive"
onClick={async () => {
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");
});
}}
>
<Button
variant="outline"
isLoading={data?.composeStatus === "running"}
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center">
<HardDriveDownload className="size-4 mr-1" />
Fresh Volumes
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent sideOffset={5} className="z-[60]">
<p>
Deploy with fresh volumes (removes all persistent
data)
</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</Button>
</DialogAction>
)}
{canDeploy && (
<DialogAction
title="Reload Compose"
@@ -428,6 +428,7 @@ export const composeRouter = createTRPCRouter({
applicationType: "compose",
descriptionLog: input.description || "",
server: !!compose.serverId,
freshVolumes: input.freshVolumes,
};
if (IS_CLOUD && compose.serverId) {
@@ -477,6 +478,7 @@ export const composeRouter = createTRPCRouter({
applicationType: "compose",
descriptionLog: input.description || "",
server: !!compose.serverId,
freshVolumes: input.freshVolumes,
};
if (IS_CLOUD && compose.serverId) {
jobData.serverId = compose.serverId;
@@ -44,12 +44,14 @@ const createDeploymentWorker = () =>
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") {
@@ -16,6 +16,7 @@ type DeployJob =
type: "deploy" | "redeploy";
applicationType: "compose";
serverId?: string;
freshVolumes?: boolean;
}
| {
applicationId: string;
+2
View File
@@ -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({
+24
View File
@@ -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`;