+ {(type === "application" || type === "compose") && (
+
+ )}
{(type === "application" || type === "compose") && (
)}
@@ -252,7 +259,16 @@ 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/application.ts b/apps/dokploy/server/api/routers/application.ts
index c0666fcc7..1cd659644 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,29 @@ 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 9354988a8..7e5ccf52e 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,27 @@ 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/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..279a089aa 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,
+ };
+};