Skip to content

Commit 8713d3e

Browse files
authored
Merge pull request #3185 from mcfdez/feat/add-delete-old-deployments
feat(deployments): add ability to delete old deployments
2 parents 32ed0c7 + 76038f6 commit 8713d3e

7 files changed

Lines changed: 197 additions & 5 deletions

File tree

apps/dokploy/components/dashboard/application/deployments/cancel-queues.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Paintbrush } from "lucide-react";
1+
import { Ban } from "lucide-react";
22
import { toast } from "sonner";
33
import {
44
AlertDialog,
@@ -35,7 +35,7 @@ export const CancelQueues = ({ id, type }: Props) => {
3535
<AlertDialogTrigger asChild>
3636
<Button variant="destructive" className="w-fit" isLoading={isLoading}>
3737
Cancel Queues
38-
<Paintbrush className="size-4" />
38+
<Ban className="size-4" />
3939
</Button>
4040
</AlertDialogTrigger>
4141
<AlertDialogContent>
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { Paintbrush } from "lucide-react";
2+
import { toast } from "sonner";
3+
import {
4+
AlertDialog,
5+
AlertDialogAction,
6+
AlertDialogCancel,
7+
AlertDialogContent,
8+
AlertDialogDescription,
9+
AlertDialogFooter,
10+
AlertDialogHeader,
11+
AlertDialogTitle,
12+
AlertDialogTrigger,
13+
} from "@/components/ui/alert-dialog";
14+
import { Button } from "@/components/ui/button";
15+
import { api } from "@/utils/api";
16+
17+
interface Props {
18+
id: string;
19+
type: "application" | "compose";
20+
}
21+
22+
export const ClearDeployments = ({ id, type }: Props) => {
23+
const utils = api.useUtils();
24+
const { mutateAsync, isLoading } =
25+
type === "application"
26+
? api.application.clearDeployments.useMutation()
27+
: api.compose.clearDeployments.useMutation();
28+
29+
return (
30+
<AlertDialog>
31+
<AlertDialogTrigger asChild>
32+
<Button variant="outline" className="w-fit" isLoading={isLoading}>
33+
Clear deployments
34+
<Paintbrush className="size-4" />
35+
</Button>
36+
</AlertDialogTrigger>
37+
<AlertDialogContent>
38+
<AlertDialogHeader>
39+
<AlertDialogTitle>
40+
Are you sure you want to clear old deployments?
41+
</AlertDialogTitle>
42+
<AlertDialogDescription>
43+
This will delete all old deployment records and logs, keeping only
44+
the active deployment (the most recent successful one).
45+
</AlertDialogDescription>
46+
</AlertDialogHeader>
47+
<AlertDialogFooter>
48+
<AlertDialogCancel>Cancel</AlertDialogCancel>
49+
<AlertDialogAction
50+
onClick={async () => {
51+
await mutateAsync({
52+
applicationId: id || "",
53+
composeId: id || "",
54+
})
55+
.then(async () => {
56+
toast.success("Old deployments cleared successfully");
57+
await utils.deployment.allByType.invalidate({
58+
id,
59+
type: type as "application" | "compose",
60+
});
61+
})
62+
.catch((err) => {
63+
toast.error(err.message);
64+
});
65+
}}
66+
>
67+
Confirm
68+
</AlertDialogAction>
69+
</AlertDialogFooter>
70+
</AlertDialogContent>
71+
</AlertDialog>
72+
);
73+
};

apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
RefreshCcw,
77
RocketIcon,
88
Settings,
9+
Trash2,
910
} from "lucide-react";
1011
import React, { useEffect, useMemo, useState } from "react";
1112
import { toast } from "sonner";
@@ -25,6 +26,7 @@ import {
2526
import { api, type RouterOutputs } from "@/utils/api";
2627
import { ShowRollbackSettings } from "../rollbacks/show-rollback-settings";
2728
import { CancelQueues } from "./cancel-queues";
29+
import { ClearDeployments } from "./clear-deployments";
2830
import { KillBuild } from "./kill-build";
2931
import { RefreshToken } from "./refresh-token";
3032
import { ShowDeployment } from "./show-deployment";
@@ -77,6 +79,8 @@ export const ShowDeployments = ({
7779
api.rollback.rollback.useMutation();
7880
const { mutateAsync: killProcess, isLoading: isKillingProcess } =
7981
api.deployment.killProcess.useMutation();
82+
const { mutateAsync: removeDeployment, isLoading: isRemovingDeployment } =
83+
api.deployment.removeDeployment.useMutation();
8084

8185
// Cancel deployment mutations
8286
const {
@@ -144,6 +148,9 @@ export const ShowDeployments = ({
144148
</CardDescription>
145149
</div>
146150
<div className="flex flex-row items-center flex-wrap gap-2">
151+
{(type === "application" || type === "compose") && (
152+
<ClearDeployments id={id} type={type} />
153+
)}
147154
{(type === "application" || type === "compose") && (
148155
<KillBuild id={id} type={type} />
149156
)}
@@ -252,6 +259,8 @@ export const ShowDeployments = ({
252259
const isExpanded = expandedDescriptions.has(
253260
deployment.deploymentId,
254261
);
262+
const canDelete =
263+
deployment.status === "done" || deployment.status === "error";
255264

256265
return (
257266
<div
@@ -370,6 +379,33 @@ export const ShowDeployments = ({
370379
View
371380
</Button>
372381

382+
{canDelete && (
383+
<DialogAction
384+
title="Delete Deployment"
385+
description="Are you sure you want to delete this deployment? This action cannot be undone."
386+
type="default"
387+
onClick={async () => {
388+
try {
389+
await removeDeployment({
390+
deploymentId: deployment.deploymentId,
391+
});
392+
toast.success("Deployment deleted successfully");
393+
} catch (error) {
394+
toast.error("Error deleting deployment");
395+
}
396+
}}
397+
>
398+
<Button
399+
variant="destructive"
400+
size="sm"
401+
isLoading={isRemovingDeployment}
402+
>
403+
Delete
404+
<Trash2 className="size-4" />
405+
</Button>
406+
</DialogAction>
407+
)}
408+
373409
{deployment?.rollback &&
374410
deployment.status === "done" &&
375411
type === "application" && (

apps/dokploy/server/api/routers/application.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
addNewService,
33
checkServiceAccess,
4+
clearOldDeployments,
45
createApplication,
56
deleteAllMiddlewares,
67
findApplicationById,
@@ -746,6 +747,23 @@ export const applicationRouter = createTRPCRouter({
746747
}
747748
await cleanQueuesByApplication(input.applicationId);
748749
}),
750+
clearDeployments: protectedProcedure
751+
.input(apiFindOneApplication)
752+
.mutation(async ({ input, ctx }) => {
753+
const application = await findApplicationById(input.applicationId);
754+
if (
755+
application.environment.project.organizationId !==
756+
ctx.session.activeOrganizationId
757+
) {
758+
throw new TRPCError({
759+
code: "UNAUTHORIZED",
760+
message:
761+
"You are not authorized to clear deployments for this application",
762+
});
763+
}
764+
await clearOldDeployments(application.appName, application.serverId);
765+
return true;
766+
}),
749767
killBuild: protectedProcedure
750768
.input(apiFindOneApplication)
751769
.mutation(async ({ input, ctx }) => {

apps/dokploy/server/api/routers/compose.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
addDomainToCompose,
33
addNewService,
44
checkServiceAccess,
5+
clearOldDeployments,
56
cloneCompose,
67
createCommand,
78
createCompose,
@@ -263,6 +264,23 @@ export const composeRouter = createTRPCRouter({
263264
await cleanQueuesByCompose(input.composeId);
264265
return { success: true, message: "Queues cleaned successfully" };
265266
}),
267+
clearDeployments: protectedProcedure
268+
.input(apiFindCompose)
269+
.mutation(async ({ input, ctx }) => {
270+
const compose = await findComposeById(input.composeId);
271+
if (
272+
compose.environment.project.organizationId !==
273+
ctx.session.activeOrganizationId
274+
) {
275+
throw new TRPCError({
276+
code: "UNAUTHORIZED",
277+
message:
278+
"You are not authorized to clear deployments for this compose",
279+
});
280+
}
281+
await clearOldDeployments(compose.appName, compose.serverId);
282+
return true;
283+
}),
266284
killBuild: protectedProcedure
267285
.input(apiFindCompose)
268286
.mutation(async ({ input, ctx }) => {

apps/dokploy/server/api/routers/deployment.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
findComposeById,
99
findDeploymentById,
1010
findServerById,
11+
removeDeployment,
1112
updateDeploymentStatus,
1213
} from "@dokploy/server";
1314
import { TRPCError } from "@trpc/server";
@@ -107,4 +108,14 @@ export const deploymentRouter = createTRPCRouter({
107108

108109
await updateDeploymentStatus(deployment.deploymentId, "error");
109110
}),
111+
112+
removeDeployment: protectedProcedure
113+
.input(
114+
z.object({
115+
deploymentId: z.string().min(1),
116+
}),
117+
)
118+
.mutation(async ({ input }) => {
119+
return await removeDeployment(input.deploymentId);
120+
}),
110121
});

packages/server/src/services/deployment.ts

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ import {
1313
deployments,
1414
} from "@dokploy/server/db/schema";
1515
import { removeDirectoryIfExistsContent } from "@dokploy/server/utils/filesystem/directory";
16-
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
16+
import {
17+
execAsync,
18+
execAsyncRemote,
19+
} from "@dokploy/server/utils/process/execAsync";
1720
import { TRPCError } from "@trpc/server";
1821
import { format } from "date-fns";
1922
import { desc, eq } from "drizzle-orm";
@@ -554,8 +557,25 @@ export const removeDeployment = async (deploymentId: string) => {
554557
const deployment = await db
555558
.delete(deployments)
556559
.where(eq(deployments.deploymentId, deploymentId))
557-
.returning();
558-
return deployment[0];
560+
.returning()
561+
.then((result) => result[0]);
562+
563+
if (!deployment) {
564+
throw new TRPCError({
565+
code: "BAD_REQUEST",
566+
message: "Deployment not found",
567+
});
568+
}
569+
const command = `
570+
rm -f ${deployment.logPath};
571+
`;
572+
if (deployment.serverId) {
573+
await execAsyncRemote(deployment.serverId, command);
574+
} else {
575+
await execAsync(command);
576+
}
577+
578+
return deployment;
559579
} catch (error) {
560580
const message =
561581
error instanceof Error ? error.message : "Error creating the deployment";
@@ -831,3 +851,19 @@ export const findAllDeploymentsByServerId = async (serverId: string) => {
831851
});
832852
return deploymentsList;
833853
};
854+
855+
export const clearOldDeployments = async (
856+
appName: string,
857+
serverId: string | null,
858+
) => {
859+
const { LOGS_PATH } = paths(!!serverId);
860+
const folder = path.join(LOGS_PATH, appName);
861+
const command = `
862+
rm -rf ${folder};
863+
`;
864+
if (serverId) {
865+
await execAsyncRemote(serverId, command);
866+
} else {
867+
await execAsync(command);
868+
}
869+
};

0 commit comments

Comments
 (0)