From 8c8a3c422721778b5ba17d4b75c6bf47ebc5deeb Mon Sep 17 00:00:00 2001 From: Marc Fernandez Date: Sun, 7 Dec 2025 12:39:31 +0100 Subject: [PATCH 1/4] feat: add ability to delete old deployments --- .../application/deployments/cancel-queues.tsx | 4 +- .../deployments/clear-deployments.tsx | 78 +++++++++++++ .../deployments/show-deployments.tsx | 4 + .../dokploy/server/api/routers/application.ts | 21 ++++ apps/dokploy/server/api/routers/compose.ts | 21 ++++ packages/server/src/services/deployment.ts | 108 ++++++++++++++++++ 6 files changed, 234 insertions(+), 2 deletions(-) create mode 100644 apps/dokploy/components/dashboard/application/deployments/clear-deployments.tsx diff --git a/apps/dokploy/components/dashboard/application/deployments/cancel-queues.tsx b/apps/dokploy/components/dashboard/application/deployments/cancel-queues.tsx index e957a496c9..6e19767b7a 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 0000000000..d0f695ddb0 --- /dev/null +++ b/apps/dokploy/components/dashboard/application/deployments/clear-deployments.tsx @@ -0,0 +1,78 @@ +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(); + const { data: isCloud } = api.settings.isCloud.useQuery(); + + if (isCloud) { + return null; + } + + 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 (result) => { + toast.success(`${result.deletedCount} old deployments cleared successfully`); + // Invalidate deployment queries to refresh the list + await utils.deployment.allByType.invalidate({ + id, + type, + }); + }) + .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 ca7a93518e..2801d904b1 100644 --- a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx +++ b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx @@ -25,6 +25,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"; @@ -144,6 +145,9 @@ export const ShowDeployments = ({
+ {(type === "application" || type === "compose") && ( + + )} {(type === "application" || type === "compose") && ( )} diff --git a/apps/dokploy/server/api/routers/application.ts b/apps/dokploy/server/api/routers/application.ts index c0666fcc78..71d5dadb37 100644 --- a/apps/dokploy/server/api/routers/application.ts +++ b/apps/dokploy/server/api/routers/application.ts @@ -1,6 +1,7 @@ import { addNewService, checkServiceAccess, + clearOldDeploymentsByApplicationId, createApplication, deleteAllMiddlewares, findApplicationById, @@ -734,6 +735,26 @@ 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", + }); + } + const result = await clearOldDeploymentsByApplicationId(input.applicationId); + return { + success: true, + message: `${result.deletedCount} old deployments cleared successfully`, + deletedCount: result.deletedCount, + }; + }), 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 9354988a8f..2b548e1f08 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, + clearOldDeploymentsByComposeId, cloneCompose, createCommand, createCompose, @@ -252,6 +253,26 @@ 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", + }); + } + const result = await clearOldDeploymentsByComposeId(input.composeId); + return { + success: true, + message: `${result.deletedCount} old deployments cleared successfully`, + deletedCount: result.deletedCount, + }; + }), killBuild: protectedProcedure .input(apiFindCompose) .mutation(async ({ input, ctx }) => { diff --git a/packages/server/src/services/deployment.ts b/packages/server/src/services/deployment.ts index 6244ec8eb6..1ba477cf06 100644 --- a/packages/server/src/services/deployment.ts +++ b/packages/server/src/services/deployment.ts @@ -831,3 +831,111 @@ export const findAllDeploymentsByServerId = async (serverId: string) => { }); return deploymentsList; }; + +export const clearOldDeploymentsByApplicationId = async ( + applicationId: string, +) => { + // Get all deployments ordered by creation date (newest first) + const deploymentsList = await db.query.deployments.findMany({ + where: eq(deployments.applicationId, applicationId), + orderBy: desc(deployments.createdAt), + }); + + // Find the most recent successful deployment (status "done") + const activeDeployment = deploymentsList.find( + (deployment) => deployment.status === "done", + ); + + // If there's an active deployment, keep it and remove all others + // If there's no active deployment, keep the most recent one and remove the rest + let deploymentsToKeep: string[] = []; + + if (activeDeployment) { + deploymentsToKeep.push(activeDeployment.deploymentId); + } else if (deploymentsList.length > 0) { + // Keep the most recent deployment even if it's not "done" + deploymentsToKeep.push(deploymentsList[0]!.deploymentId); + } + + const deploymentsToDelete = deploymentsList.filter( + (deployment) => !deploymentsToKeep.includes(deployment.deploymentId), + ); + + // Delete old deployments and their log files + for (const deployment of deploymentsToDelete) { + if (deployment.rollbackId) { + await removeRollbackById(deployment.rollbackId); + } + + // Remove log file if it exists + const logPath = deployment.logPath; + if (logPath && logPath !== "." && existsSync(logPath)) { + try { + await fsPromises.unlink(logPath); + } catch (error) { + console.error(`Error removing log file ${logPath}:`, error); + } + } + + // Delete deployment from database + await removeDeployment(deployment.deploymentId); + } + + return { + deletedCount: deploymentsToDelete.length, + keptDeployment: deploymentsToKeep[0] || null, + }; +}; + +export const clearOldDeploymentsByComposeId = async (composeId: string) => { + // Get all deployments ordered by creation date (newest first) + const deploymentsList = await db.query.deployments.findMany({ + where: eq(deployments.composeId, composeId), + orderBy: desc(deployments.createdAt), + }); + + // Find the most recent successful deployment (status "done") + const activeDeployment = deploymentsList.find( + (deployment) => deployment.status === "done", + ); + + // If there's an active deployment, keep it and remove all others + // If there's no active deployment, keep the most recent one and remove the rest + let deploymentsToKeep: string[] = []; + + if (activeDeployment) { + deploymentsToKeep.push(activeDeployment.deploymentId); + } else if (deploymentsList.length > 0) { + // Keep the most recent deployment even if it's not "done" + deploymentsToKeep.push(deploymentsList[0]!.deploymentId); + } + + const deploymentsToDelete = deploymentsList.filter( + (deployment) => !deploymentsToKeep.includes(deployment.deploymentId), + ); + + // Delete old deployments and their log files + for (const deployment of deploymentsToDelete) { + if (deployment.rollbackId) { + await removeRollbackById(deployment.rollbackId); + } + + // Remove log file if it exists + const logPath = deployment.logPath; + if (logPath && logPath !== "." && existsSync(logPath)) { + try { + await fsPromises.unlink(logPath); + } catch (error) { + console.error(`Error removing log file ${logPath}:`, error); + } + } + + // Delete deployment from database + await removeDeployment(deployment.deploymentId); + } + + return { + deletedCount: deploymentsToDelete.length, + keptDeployment: deploymentsToKeep[0] || null, + }; +}; From 270ebd6b50fc5556e14596f197d013aba12d4175 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 8 Dec 2025 02:30:23 +0000 Subject: [PATCH 2/4] [autofix.ci] apply automated fixes --- .../deployments/clear-deployments.tsx | 131 +++++++++--------- .../dokploy/server/api/routers/application.ts | 7 +- apps/dokploy/server/api/routers/compose.ts | 3 +- packages/server/src/services/deployment.ts | 12 +- 4 files changed, 80 insertions(+), 73 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/deployments/clear-deployments.tsx b/apps/dokploy/components/dashboard/application/deployments/clear-deployments.tsx index d0f695ddb0..70f9d2554d 100644 --- a/apps/dokploy/components/dashboard/application/deployments/clear-deployments.tsx +++ b/apps/dokploy/components/dashboard/application/deployments/clear-deployments.tsx @@ -1,78 +1,81 @@ import { Paintbrush } from "lucide-react"; import { toast } from "sonner"; import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, + 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"; + 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(); - const { data: isCloud } = api.settings.isCloud.useQuery(); + const utils = api.useUtils(); + const { mutateAsync, isLoading } = + type === "application" + ? api.application.clearDeployments.useMutation() + : api.compose.clearDeployments.useMutation(); + const { data: isCloud } = api.settings.isCloud.useQuery(); - if (isCloud) { - return null; - } + if (isCloud) { + return null; + } - 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 (result) => { - toast.success(`${result.deletedCount} old deployments cleared successfully`); - // Invalidate deployment queries to refresh the list - await utils.deployment.allByType.invalidate({ - id, - type, - }); - }) - .catch((err) => { - toast.error(err.message); - }); - }} - > - Confirm - - - - - ); + 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 (result) => { + toast.success( + `${result.deletedCount} old deployments cleared successfully`, + ); + // Invalidate deployment queries to refresh the list + await utils.deployment.allByType.invalidate({ + id, + type, + }); + }) + .catch((err) => { + toast.error(err.message); + }); + }} + > + Confirm + + + + + ); }; diff --git a/apps/dokploy/server/api/routers/application.ts b/apps/dokploy/server/api/routers/application.ts index 71d5dadb37..1cd6596447 100644 --- a/apps/dokploy/server/api/routers/application.ts +++ b/apps/dokploy/server/api/routers/application.ts @@ -745,10 +745,13 @@ export const applicationRouter = createTRPCRouter({ ) { throw new TRPCError({ code: "UNAUTHORIZED", - message: "You are not authorized to clear deployments for this application", + message: + "You are not authorized to clear deployments for this application", }); } - const result = await clearOldDeploymentsByApplicationId(input.applicationId); + const result = await clearOldDeploymentsByApplicationId( + input.applicationId, + ); return { success: true, message: `${result.deletedCount} old deployments cleared successfully`, diff --git a/apps/dokploy/server/api/routers/compose.ts b/apps/dokploy/server/api/routers/compose.ts index 2b548e1f08..7e5ccf52e5 100644 --- a/apps/dokploy/server/api/routers/compose.ts +++ b/apps/dokploy/server/api/routers/compose.ts @@ -263,7 +263,8 @@ export const composeRouter = createTRPCRouter({ ) { throw new TRPCError({ code: "UNAUTHORIZED", - message: "You are not authorized to clear deployments for this compose", + message: + "You are not authorized to clear deployments for this compose", }); } const result = await clearOldDeploymentsByComposeId(input.composeId); diff --git a/packages/server/src/services/deployment.ts b/packages/server/src/services/deployment.ts index 1ba477cf06..279a089aab 100644 --- a/packages/server/src/services/deployment.ts +++ b/packages/server/src/services/deployment.ts @@ -849,7 +849,7 @@ export const clearOldDeploymentsByApplicationId = async ( // If there's an active deployment, keep it and remove all others // If there's no active deployment, keep the most recent one and remove the rest let deploymentsToKeep: string[] = []; - + if (activeDeployment) { deploymentsToKeep.push(activeDeployment.deploymentId); } else if (deploymentsList.length > 0) { @@ -866,7 +866,7 @@ export const clearOldDeploymentsByApplicationId = async ( if (deployment.rollbackId) { await removeRollbackById(deployment.rollbackId); } - + // Remove log file if it exists const logPath = deployment.logPath; if (logPath && logPath !== "." && existsSync(logPath)) { @@ -876,7 +876,7 @@ export const clearOldDeploymentsByApplicationId = async ( console.error(`Error removing log file ${logPath}:`, error); } } - + // Delete deployment from database await removeDeployment(deployment.deploymentId); } @@ -902,7 +902,7 @@ export const clearOldDeploymentsByComposeId = async (composeId: string) => { // If there's an active deployment, keep it and remove all others // If there's no active deployment, keep the most recent one and remove the rest let deploymentsToKeep: string[] = []; - + if (activeDeployment) { deploymentsToKeep.push(activeDeployment.deploymentId); } else if (deploymentsList.length > 0) { @@ -919,7 +919,7 @@ export const clearOldDeploymentsByComposeId = async (composeId: string) => { if (deployment.rollbackId) { await removeRollbackById(deployment.rollbackId); } - + // Remove log file if it exists const logPath = deployment.logPath; if (logPath && logPath !== "." && existsSync(logPath)) { @@ -929,7 +929,7 @@ export const clearOldDeploymentsByComposeId = async (composeId: string) => { console.error(`Error removing log file ${logPath}:`, error); } } - + // Delete deployment from database await removeDeployment(deployment.deploymentId); } From e4baeb7fdfcab1ac47341da0917905311fe35119 Mon Sep 17 00:00:00 2001 From: Marc Fernandez Date: Sun, 21 Dec 2025 16:29:14 +0100 Subject: [PATCH 3/4] feat: add individual deployment deletion --- .../deployments/show-deployments.tsx | 40 ++++++++++++++++++- apps/dokploy/server/api/routers/deployment.ts | 11 +++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx index 2801d904b1..d0fca3cf4b 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"; @@ -78,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 { @@ -256,7 +259,15 @@ export const ShowDeployments = ({ const isExpanded = expandedDescriptions.has( deployment.deploymentId, ); - + const lastSuccessfulDeployment = deployments?.find( + (d) => d.status === "done" + ); + const isLastSuccessfulDeployment = + lastSuccessfulDeployment?.deploymentId === deployment.deploymentId; + const canDelete = + deployments && + deployments.length > 1 && + !isLastSuccessfulDeployment; 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/deployment.ts b/apps/dokploy/server/api/routers/deployment.ts index 9004a0a054..d9fab404f4 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); + }), }); From 483b8d3943af584992e1890d44d06ff21413162a Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 9 Jan 2026 06:15:41 +0000 Subject: [PATCH 4/4] [autofix.ci] apply automated fixes --- .../application/deployments/show-deployments.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx index d0fca3cf4b..057f481e6c 100644 --- a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx +++ b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx @@ -260,13 +260,14 @@ export const ShowDeployments = ({ deployment.deploymentId, ); const lastSuccessfulDeployment = deployments?.find( - (d) => d.status === "done" + (d) => d.status === "done", ); - const isLastSuccessfulDeployment = - lastSuccessfulDeployment?.deploymentId === deployment.deploymentId; - const canDelete = - deployments && - deployments.length > 1 && + const isLastSuccessfulDeployment = + lastSuccessfulDeployment?.deploymentId === + deployment.deploymentId; + const canDelete = + deployments && + deployments.length > 1 && !isLastSuccessfulDeployment; return (