diff --git a/apps/dokploy/components/dashboard/application/deployments/cancel-queues.tsx b/apps/dokploy/components/dashboard/application/deployments/cancel-queues.tsx index e957a496c..6e19767b7 100644 --- a/apps/dokploy/components/dashboard/application/deployments/cancel-queues.tsx +++ b/apps/dokploy/components/dashboard/application/deployments/cancel-queues.tsx @@ -1,4 +1,4 @@ -import { Paintbrush } from "lucide-react"; +import { Ban } from "lucide-react"; import { toast } from "sonner"; import { AlertDialog, @@ -35,7 +35,7 @@ export const CancelQueues = ({ id, type }: Props) => { diff --git a/apps/dokploy/components/dashboard/application/deployments/clear-deployments.tsx b/apps/dokploy/components/dashboard/application/deployments/clear-deployments.tsx new file mode 100644 index 000000000..75862dfff --- /dev/null +++ b/apps/dokploy/components/dashboard/application/deployments/clear-deployments.tsx @@ -0,0 +1,73 @@ +import { Paintbrush } from "lucide-react"; +import { toast } from "sonner"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog"; +import { Button } from "@/components/ui/button"; +import { api } from "@/utils/api"; + +interface Props { + id: string; + type: "application" | "compose"; +} + +export const ClearDeployments = ({ id, type }: Props) => { + const utils = api.useUtils(); + const { mutateAsync, isLoading } = + type === "application" + ? api.application.clearDeployments.useMutation() + : api.compose.clearDeployments.useMutation(); + + return ( + + + + + + + + Are you sure you want to clear old deployments? + + + This will delete all old deployment records and logs, keeping only + the active deployment (the most recent successful one). + + + + Cancel + { + await mutateAsync({ + applicationId: id || "", + composeId: id || "", + }) + .then(async () => { + toast.success("Old deployments cleared successfully"); + await utils.deployment.allByType.invalidate({ + id, + type: type as "application" | "compose", + }); + }) + .catch((err) => { + toast.error(err.message); + }); + }} + > + Confirm + + + + + ); +}; diff --git a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx index cfe747d27..3f674aa3c 100644 --- a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx +++ b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx @@ -6,6 +6,7 @@ import { RefreshCcw, RocketIcon, Settings, + Trash2, } from "lucide-react"; import React, { useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; @@ -25,6 +26,7 @@ import { import { api, type RouterOutputs } from "@/utils/api"; import { ShowRollbackSettings } from "../rollbacks/show-rollback-settings"; import { CancelQueues } from "./cancel-queues"; +import { ClearDeployments } from "./clear-deployments"; import { KillBuild } from "./kill-build"; import { RefreshToken } from "./refresh-token"; import { ShowDeployment } from "./show-deployment"; @@ -77,6 +79,8 @@ export const ShowDeployments = ({ api.rollback.rollback.useMutation(); const { mutateAsync: killProcess, isLoading: isKillingProcess } = api.deployment.killProcess.useMutation(); + const { mutateAsync: removeDeployment, isLoading: isRemovingDeployment } = + api.deployment.removeDeployment.useMutation(); // Cancel deployment mutations const { @@ -144,6 +148,9 @@ export const ShowDeployments = ({
+ {(type === "application" || type === "compose") && ( + + )} {(type === "application" || type === "compose") && ( )} @@ -252,6 +259,8 @@ export const ShowDeployments = ({ const isExpanded = expandedDescriptions.has( deployment.deploymentId, ); + const canDelete = + deployment.status === "done" || deployment.status === "error"; return (
+ {canDelete && ( + { + try { + await removeDeployment({ + deploymentId: deployment.deploymentId, + }); + toast.success("Deployment deleted successfully"); + } catch (error) { + toast.error("Error deleting deployment"); + } + }} + > + + + )} + {deployment?.rollback && deployment.status === "done" && type === "application" && ( diff --git a/apps/dokploy/server/api/routers/application.ts b/apps/dokploy/server/api/routers/application.ts index 9240a1cb3..391191ce3 100644 --- a/apps/dokploy/server/api/routers/application.ts +++ b/apps/dokploy/server/api/routers/application.ts @@ -1,6 +1,7 @@ import { addNewService, checkServiceAccess, + clearOldDeployments, createApplication, deleteAllMiddlewares, findApplicationById, @@ -746,6 +747,23 @@ export const applicationRouter = createTRPCRouter({ } await cleanQueuesByApplication(input.applicationId); }), + clearDeployments: protectedProcedure + .input(apiFindOneApplication) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if ( + application.environment.project.organizationId !== + ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: + "You are not authorized to clear deployments for this application", + }); + } + await clearOldDeployments(application.appName, application.serverId); + return true; + }), killBuild: protectedProcedure .input(apiFindOneApplication) .mutation(async ({ input, ctx }) => { diff --git a/apps/dokploy/server/api/routers/compose.ts b/apps/dokploy/server/api/routers/compose.ts index 01f9c8b04..665b6770b 100644 --- a/apps/dokploy/server/api/routers/compose.ts +++ b/apps/dokploy/server/api/routers/compose.ts @@ -2,6 +2,7 @@ import { addDomainToCompose, addNewService, checkServiceAccess, + clearOldDeployments, cloneCompose, createCommand, createCompose, @@ -263,6 +264,23 @@ export const composeRouter = createTRPCRouter({ await cleanQueuesByCompose(input.composeId); return { success: true, message: "Queues cleaned successfully" }; }), + clearDeployments: protectedProcedure + .input(apiFindCompose) + .mutation(async ({ input, ctx }) => { + const compose = await findComposeById(input.composeId); + if ( + compose.environment.project.organizationId !== + ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: + "You are not authorized to clear deployments for this compose", + }); + } + await clearOldDeployments(compose.appName, compose.serverId); + return true; + }), killBuild: protectedProcedure .input(apiFindCompose) .mutation(async ({ input, ctx }) => { diff --git a/apps/dokploy/server/api/routers/deployment.ts b/apps/dokploy/server/api/routers/deployment.ts index 9004a0a05..d9fab404f 100644 --- a/apps/dokploy/server/api/routers/deployment.ts +++ b/apps/dokploy/server/api/routers/deployment.ts @@ -8,6 +8,7 @@ import { findComposeById, findDeploymentById, findServerById, + removeDeployment, updateDeploymentStatus, } from "@dokploy/server"; import { TRPCError } from "@trpc/server"; @@ -107,4 +108,14 @@ export const deploymentRouter = createTRPCRouter({ await updateDeploymentStatus(deployment.deploymentId, "error"); }), + + removeDeployment: protectedProcedure + .input( + z.object({ + deploymentId: z.string().min(1), + }), + ) + .mutation(async ({ input }) => { + return await removeDeployment(input.deploymentId); + }), }); diff --git a/packages/server/src/services/deployment.ts b/packages/server/src/services/deployment.ts index 6244ec8eb..24e1590a9 100644 --- a/packages/server/src/services/deployment.ts +++ b/packages/server/src/services/deployment.ts @@ -13,7 +13,10 @@ import { deployments, } from "@dokploy/server/db/schema"; import { removeDirectoryIfExistsContent } from "@dokploy/server/utils/filesystem/directory"; -import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync"; +import { + execAsync, + execAsyncRemote, +} from "@dokploy/server/utils/process/execAsync"; import { TRPCError } from "@trpc/server"; import { format } from "date-fns"; import { desc, eq } from "drizzle-orm"; @@ -554,8 +557,25 @@ export const removeDeployment = async (deploymentId: string) => { const deployment = await db .delete(deployments) .where(eq(deployments.deploymentId, deploymentId)) - .returning(); - return deployment[0]; + .returning() + .then((result) => result[0]); + + if (!deployment) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Deployment not found", + }); + } + const command = ` + rm -f ${deployment.logPath}; + `; + if (deployment.serverId) { + await execAsyncRemote(deployment.serverId, command); + } else { + await execAsync(command); + } + + return deployment; } catch (error) { const message = error instanceof Error ? error.message : "Error creating the deployment"; @@ -831,3 +851,19 @@ export const findAllDeploymentsByServerId = async (serverId: string) => { }); return deploymentsList; }; + +export const clearOldDeployments = async ( + appName: string, + serverId: string | null, +) => { + const { LOGS_PATH } = paths(!!serverId); + const folder = path.join(LOGS_PATH, appName); + const command = ` + rm -rf ${folder}; + `; + if (serverId) { + await execAsyncRemote(serverId, command); + } else { + await execAsync(command); + } +};