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..d0f695ddb
--- /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 cfe747d27..159b89442 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 c0666fcc7..71d5dadb3 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 9354988a8..2b548e1f0 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 6244ec8eb..1ba477cf0 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,
+ };
+};