Merge pull request #4195 from lamualfa/feat/compose-deploy-fresh-volumes

feat: add Deploy with Fresh Volumes for Docker Compose
This commit is contained in:
Mauricio Siu
2026-09-02 00:45:06 -06:00
committed by GitHub
6 changed files with 84 additions and 1 deletions
@@ -1,4 +1,11 @@
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 { Tooltip as TooltipPrimitive } from "radix-ui";
import { toast } from "sonner";
@@ -82,6 +89,51 @@ 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="Rebuild Compose"
@@ -428,6 +428,7 @@ export const composeRouter = createTRPCRouter({
descriptionLog: input.description || "",
server: !!compose.serverId,
serverId: compose.serverId ?? undefined,
freshVolumes: input.freshVolumes,
};
if (IS_CLOUD && compose.serverId) {
@@ -477,6 +478,7 @@ export const composeRouter = createTRPCRouter({
descriptionLog: input.description || "",
server: !!compose.serverId,
serverId: compose.serverId ?? undefined,
freshVolumes: input.freshVolumes,
};
if (IS_CLOUD && compose.serverId) {
deploy(jobData).catch((error) => {
@@ -42,12 +42,14 @@ export const processDeploymentJob = async (job: InMemoryJob) => {
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
@@ -237,12 +237,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
@@ -229,10 +229,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);
@@ -286,6 +288,16 @@ export const deployCompose = async ({
}
}
if (freshVolumes && compose.composeType === "docker-compose") {
const downCommand = `set -e; env -i PATH="$PATH" 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`;
@@ -359,10 +371,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);
@@ -400,6 +414,16 @@ export const rebuildCompose = async ({
}
}
if (freshVolumes && compose.composeType === "docker-compose") {
const downCommand = `set -e; env -i PATH="$PATH" 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`;