Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Paintbrush } from "lucide-react";
import { Ban } from "lucide-react";
import { toast } from "sonner";
import {
AlertDialog,
Expand Down Expand Up @@ -35,7 +35,7 @@ export const CancelQueues = ({ id, type }: Props) => {
<AlertDialogTrigger asChild>
<Button variant="destructive" className="w-fit" isLoading={isLoading}>
Cancel Queues
<Paintbrush className="size-4" />
<Ban className="size-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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 (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline" className="w-fit" isLoading={isLoading}>
Clear deployments
<Paintbrush className="size-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Are you sure you want to clear old deployments?
</AlertDialogTitle>
<AlertDialogDescription>
This will delete all old deployment records and logs, keeping only
the active deployment (the most recent successful one).
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
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
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
RefreshCcw,
RocketIcon,
Settings,
Trash2,
} from "lucide-react";
import React, { useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
Expand All @@ -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";
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -144,6 +148,9 @@ export const ShowDeployments = ({
</CardDescription>
</div>
<div className="flex flex-row items-center flex-wrap gap-2">
{(type === "application" || type === "compose") && (
<ClearDeployments id={id} type={type} />
)}
{(type === "application" || type === "compose") && (
<KillBuild id={id} type={type} />
)}
Expand Down Expand Up @@ -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 (
<div
key={deployment.deploymentId}
Expand Down Expand Up @@ -368,6 +384,33 @@ export const ShowDeployments = ({
View
</Button>

{canDelete && (
<DialogAction
title="Delete Deployment"
description="Are you sure you want to delete this deployment? This action cannot be undone."
type="default"
onClick={async () => {
try {
await removeDeployment({
deploymentId: deployment.deploymentId,
});
toast.success("Deployment deleted successfully");
} catch (error) {
toast.error("Error deleting deployment");
}
}}
>
<Button
variant="destructive"
size="sm"
isLoading={isRemovingDeployment}
>
Delete
<Trash2 className="size-4" />
</Button>
</DialogAction>
)}

{deployment?.rollback &&
deployment.status === "done" &&
type === "application" && (
Expand Down
24 changes: 24 additions & 0 deletions apps/dokploy/server/api/routers/application.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
addNewService,
checkServiceAccess,
clearOldDeploymentsByApplicationId,
createApplication,
deleteAllMiddlewares,
findApplicationById,
Expand Down Expand Up @@ -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 }) => {
Expand Down
22 changes: 22 additions & 0 deletions apps/dokploy/server/api/routers/compose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
addDomainToCompose,
addNewService,
checkServiceAccess,
clearOldDeploymentsByComposeId,
cloneCompose,
createCommand,
createCompose,
Expand Down Expand Up @@ -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 }) => {
Expand Down
11 changes: 11 additions & 0 deletions apps/dokploy/server/api/routers/deployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
findComposeById,
findDeploymentById,
findServerById,
removeDeployment,
updateDeploymentStatus,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
Expand Down Expand Up @@ -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);
}),
});
108 changes: 108 additions & 0 deletions packages/server/src/services/deployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
};