diff --git a/apps/dokploy/components/dashboard/application/deployments/kill-build.tsx b/apps/dokploy/components/dashboard/application/deployments/kill-build.tsx
new file mode 100644
index 000000000..784534dd6
--- /dev/null
+++ b/apps/dokploy/components/dashboard/application/deployments/kill-build.tsx
@@ -0,0 +1,65 @@
+import { Scissors } 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 KillBuild = ({ id, type }: Props) => {
+ const { mutateAsync, isLoading } =
+ type === "application"
+ ? api.application.killBuild.useMutation()
+ : api.compose.killBuild.useMutation();
+
+ return (
+
+
+
+
+
+
+ Are you sure to kill the build?
+
+ This will kill the build process
+
+
+
+ Cancel
+ {
+ await mutateAsync({
+ applicationId: id || "",
+ composeId: id || "",
+ })
+ .then(() => {
+ toast.success("Build killed successfully");
+ })
+ .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 1885ffc3a..7f3bc82b4 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 { KillBuild } from "./kill-build";
import { RefreshToken } from "./refresh-token";
import { ShowDeployment } from "./show-deployment";
@@ -143,6 +144,9 @@ export const ShowDeployments = ({
+ {(type === "application" || type === "compose") && (
+
+ )}
{(type === "application" || type === "compose") && (
)}
diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx
index d93bbd1c8..9c2e48931 100644
--- a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx
+++ b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx
@@ -182,7 +182,16 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
id={deployment.previewDeploymentId}
type="previewDeployment"
serverId={data?.serverId || ""}
- />
+ >
+
+
Schedule volume backups to run automatically at specified
- intervals.
+ intervals
diff --git a/apps/dokploy/components/dashboard/projects/show.tsx b/apps/dokploy/components/dashboard/projects/show.tsx
index 783c5bb32..5369a544e 100644
--- a/apps/dokploy/components/dashboard/projects/show.tsx
+++ b/apps/dokploy/components/dashboard/projects/show.tsx
@@ -14,6 +14,7 @@ import { useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { DateTooltip } from "@/components/shared/date-tooltip";
+import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import {
AlertDialog,
@@ -44,7 +45,6 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
-import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input";
import {
Select,
SelectContent,
@@ -52,12 +52,14 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
+import { TimeBadge } from "@/components/ui/time-badge";
import { api } from "@/utils/api";
import { HandleProject } from "./handle-project";
import { ProjectEnvironment } from "./project-environment";
export const ShowProjects = () => {
const utils = api.useUtils();
+ const { data: isCloud } = api.settings.isCloud.useQuery();
const { data, isLoading } = api.project.all.useQuery();
const { data: auth } = api.user.get.useQuery();
const { mutateAsync } = api.project.remove.useMutation();
@@ -135,6 +137,11 @@ export const ShowProjects = () => {
+ {!isCloud && (
+
+
+
+ )}
@@ -148,7 +155,6 @@ export const ShowProjects = () => {
Create and manage your projects
-
{(auth?.role === "owner" || auth?.canCreateProjects) && (
@@ -298,7 +304,13 @@ export const ShowProjects = () => {
{domain.host}
@@ -340,7 +352,13 @@ export const ShowProjects = () => {
{domain.host}
diff --git a/apps/dokploy/components/layouts/side.tsx b/apps/dokploy/components/layouts/side.tsx
index 8d84e260c..7473fe586 100644
--- a/apps/dokploy/components/layouts/side.tsx
+++ b/apps/dokploy/components/layouts/side.tsx
@@ -83,6 +83,7 @@ import { AddOrganization } from "../dashboard/organization/handle-organization";
import { DialogAction } from "../shared/dialog-action";
import { Logo } from "../shared/logo";
import { Button } from "../ui/button";
+import { TimeBadge } from "../ui/time-badge";
import { UpdateServerButton } from "./update-server";
import { UserNav } from "./user-nav";
@@ -1125,6 +1126,7 @@ export default function Page({ children }: Props) {
+ {!isCloud &&
}
)}
diff --git a/apps/dokploy/components/ui/time-badge.tsx b/apps/dokploy/components/ui/time-badge.tsx
new file mode 100644
index 000000000..ea7f1f84e
--- /dev/null
+++ b/apps/dokploy/components/ui/time-badge.tsx
@@ -0,0 +1,58 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { api } from "@/utils/api";
+
+export function TimeBadge() {
+ const { data: serverTime } = api.server.getServerTime.useQuery(undefined);
+ const [time, setTime] = useState(null);
+
+ useEffect(() => {
+ if (serverTime?.time) {
+ setTime(new Date(serverTime.time));
+ }
+ }, [serverTime]);
+
+ useEffect(() => {
+ const timer = setInterval(() => {
+ setTime((prevTime) => {
+ if (!prevTime) return null;
+ const newTime = new Date(prevTime.getTime() + 1000);
+ return newTime;
+ });
+ }, 1000);
+
+ return () => {
+ clearInterval(timer);
+ };
+ }, []);
+
+ if (!time || !serverTime?.timezone) {
+ return null;
+ }
+
+ const getUtcOffset = (timeZone: string) => {
+ const date = new Date();
+ const utcDate = new Date(date.toLocaleString("en-US", { timeZone: "UTC" }));
+ const tzDate = new Date(date.toLocaleString("en-US", { timeZone }));
+ const offset = (tzDate.getTime() - utcDate.getTime()) / (1000 * 60 * 60);
+ const sign = offset >= 0 ? "+" : "-";
+ const hours = Math.floor(Math.abs(offset));
+ const minutes = (Math.abs(offset) * 60) % 60;
+ return `UTC${sign}${hours.toString().padStart(2, "0")}:${minutes
+ .toString()
+ .padStart(2, "0")}`;
+ };
+
+ return (
+
+ Server Time:
+
+ {time.toLocaleTimeString()}
+
+
+ ({serverTime.timezone} | {getUtcOffset(serverTime.timezone)})
+
+
+ );
+}
diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json
index c9addf8de..070a0d6a4 100644
--- a/apps/dokploy/package.json
+++ b/apps/dokploy/package.json
@@ -1,6 +1,6 @@
{
"name": "dokploy",
- "version": "v0.25.6",
+ "version": "v0.25.8",
"private": true,
"license": "Apache-2.0",
"type": "module",
diff --git a/apps/dokploy/server/api/routers/application.ts b/apps/dokploy/server/api/routers/application.ts
index 006d024c4..c713fd7eb 100644
--- a/apps/dokploy/server/api/routers/application.ts
+++ b/apps/dokploy/server/api/routers/application.ts
@@ -58,7 +58,11 @@ import {
applications,
} from "@/server/db/schema";
import type { DeploymentJob } from "@/server/queues/queue-types";
-import { cleanQueuesByApplication, myQueue } from "@/server/queues/queueSetup";
+import {
+ cleanQueuesByApplication,
+ killDockerBuild,
+ myQueue,
+} from "@/server/queues/queueSetup";
import { cancelDeployment, deploy } from "@/server/utils/deploy";
import { uploadFileSchema } from "@/utils/schema";
@@ -725,7 +729,21 @@ export const applicationRouter = createTRPCRouter({
}
await cleanQueuesByApplication(input.applicationId);
}),
-
+ killBuild: 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 kill this build",
+ });
+ }
+ await killDockerBuild("application", application.serverId);
+ }),
readTraefikConfig: protectedProcedure
.input(apiFindOneApplication)
.query(async ({ input, ctx }) => {
diff --git a/apps/dokploy/server/api/routers/compose.ts b/apps/dokploy/server/api/routers/compose.ts
index 026b6e8ad..e233dc6ca 100644
--- a/apps/dokploy/server/api/routers/compose.ts
+++ b/apps/dokploy/server/api/routers/compose.ts
@@ -59,7 +59,11 @@ import {
compose as composeTable,
} from "@/server/db/schema";
import type { DeploymentJob } from "@/server/queues/queue-types";
-import { cleanQueuesByCompose, myQueue } from "@/server/queues/queueSetup";
+import {
+ cleanQueuesByCompose,
+ killDockerBuild,
+ myQueue,
+} from "@/server/queues/queueSetup";
import { cancelDeployment, deploy } from "@/server/utils/deploy";
import { generatePassword } from "@/templates/utils";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
@@ -248,6 +252,21 @@ export const composeRouter = createTRPCRouter({
await cleanQueuesByCompose(input.composeId);
return { success: true, message: "Queues cleaned successfully" };
}),
+ killBuild: 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 kill this build",
+ });
+ }
+ await killDockerBuild("compose", compose.serverId);
+ }),
loadServices: protectedProcedure
.input(apiFetchServices)
diff --git a/apps/dokploy/server/api/routers/server.ts b/apps/dokploy/server/api/routers/server.ts
index d6904a7ec..8a01228f8 100644
--- a/apps/dokploy/server/api/routers/server.ts
+++ b/apps/dokploy/server/api/routers/server.ts
@@ -383,6 +383,15 @@ export const serverRouter = createTRPCRouter({
const ip = await getPublicIpWithFallback();
return ip;
}),
+ getServerTime: protectedProcedure.query(() => {
+ if (IS_CLOUD) {
+ return null;
+ }
+ return {
+ time: new Date(),
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
+ };
+ }),
getServerMetrics: protectedProcedure
.input(
z.object({
diff --git a/apps/dokploy/server/queues/queueSetup.ts b/apps/dokploy/server/queues/queueSetup.ts
index 1577273c8..351f5d1c0 100644
--- a/apps/dokploy/server/queues/queueSetup.ts
+++ b/apps/dokploy/server/queues/queueSetup.ts
@@ -1,3 +1,7 @@
+import {
+ execAsync,
+ execAsyncRemote,
+} from "@dokploy/server/utils/process/execAsync";
import { Queue } from "bullmq";
import { redisConfig } from "./redis-connection";
@@ -41,4 +45,31 @@ export const cleanQueuesByCompose = async (composeId: string) => {
}
};
+export const killDockerBuild = async (
+ type: "application" | "compose",
+ serverId: string | null,
+) => {
+ try {
+ if (type === "application") {
+ const command = `pkill -2 -f "docker build"`;
+
+ if (serverId) {
+ await execAsyncRemote(serverId, command);
+ } else {
+ await execAsync(command);
+ }
+ } else if (type === "compose") {
+ const command = `pkill -2 -f "docker compose"`;
+
+ if (serverId) {
+ await execAsyncRemote(serverId, command);
+ } else {
+ await execAsync(command);
+ }
+ }
+ } catch (error) {
+ console.error(error);
+ }
+};
+
export { myQueue };
diff --git a/packages/server/src/services/settings.ts b/packages/server/src/services/settings.ts
index 3c198d70d..a480e298c 100644
--- a/packages/server/src/services/settings.ts
+++ b/packages/server/src/services/settings.ts
@@ -59,7 +59,8 @@ export const getUpdateData = async (): Promise => {
let currentDigest: string;
try {
currentDigest = await getServiceImageDigest();
- } catch {
+ } catch (error) {
+ console.error(error);
// Docker service might not exist locally
// You can run the # Installation command for docker service create mentioned in the below docs to test it locally:
// https://docs.dokploy.com/docs/core/manual-installation
diff --git a/packages/server/src/utils/builders/compose.ts b/packages/server/src/utils/builders/compose.ts
index 7792ed11c..6ac5bf130 100644
--- a/packages/server/src/utils/builders/compose.ts
+++ b/packages/server/src/utils/builders/compose.ts
@@ -2,7 +2,7 @@ import { dirname, join } from "node:path";
import { paths } from "@dokploy/server/constants";
import type { InferResultType } from "@dokploy/server/types/with";
import boxen from "boxen";
-import { writeDomainsToComposeRemote } from "../docker/domain";
+import { writeDomainsToCompose } from "../docker/domain";
import {
encodeBase64,
getEnviromentVariablesObject,
@@ -22,7 +22,7 @@ export const getBuildComposeCommand = async (compose: ComposeNested) => {
const projectPath = join(COMPOSE_PATH, compose.appName, "code");
const exportEnvCommand = getExportEnvCommand(compose);
- const newCompose = await writeDomainsToComposeRemote(compose, domains);
+ const newCompose = await writeDomainsToCompose(compose, domains);
const logContent = `
App Name: ${appName}
Build Compose 🐳
diff --git a/packages/server/src/utils/docker/domain.ts b/packages/server/src/utils/docker/domain.ts
index a176a4560..2272f364e 100644
--- a/packages/server/src/utils/docker/domain.ts
+++ b/packages/server/src/utils/docker/domain.ts
@@ -102,7 +102,7 @@ export const readComposeFile = async (compose: Compose) => {
return null;
};
-export const writeDomainsToComposeRemote = async (
+export const writeDomainsToCompose = async (
compose: Compose,
domains: Domain[],
) => {
@@ -120,19 +120,16 @@ echo "❌ Error: Compose file not found";
exit 1;
`;
}
- if (compose.serverId) {
- const composeString = stringify(composeConverted, { lineWidth: 1000 });
- const encodedContent = encodeBase64(composeString);
- return `echo "${encodedContent}" | base64 -d > "${path}";`;
- }
+
+ const composeString = stringify(composeConverted, { lineWidth: 1000 });
+ const encodedContent = encodeBase64(composeString);
+ return `echo "${encodedContent}" | base64 -d > "${path}";`;
} catch (error) {
// @ts-ignore
- return `echo "❌ Has occured an error: ${error?.message || error}";
+ return `echo "❌ Has occurred an error: ${error?.message || error}";
exit 1;
`;
}
-
- return "";
};
export const addDomainToCompose = async (
compose: Compose,
diff --git a/packages/server/src/utils/restore/utils.ts b/packages/server/src/utils/restore/utils.ts
index c46077238..23052e642 100644
--- a/packages/server/src/utils/restore/utils.ts
+++ b/packages/server/src/utils/restore/utils.ts
@@ -7,7 +7,7 @@ export const getPostgresRestoreCommand = (
database: string,
databaseUser: string,
) => {
- return `docker exec -i $CONTAINER_ID sh -c "pg_restore -U ${databaseUser} -d ${database} -O --clean --if-exists"`;
+ return `docker exec -i $CONTAINER_ID sh -c "pg_restore -U '${databaseUser}' -d ${database} -O --clean --if-exists"`;
};
export const getMariadbRestoreCommand = (
@@ -15,14 +15,14 @@ export const getMariadbRestoreCommand = (
databaseUser: string,
databasePassword: string,
) => {
- return `docker exec -i $CONTAINER_ID sh -c "mariadb -u ${databaseUser} -p${databasePassword} ${database}"`;
+ return `docker exec -i $CONTAINER_ID sh -c "mariadb -u '${databaseUser}' -p'${databasePassword}' ${database}"`;
};
export const getMysqlRestoreCommand = (
database: string,
databasePassword: string,
) => {
- return `docker exec -i $CONTAINER_ID sh -c "mysql -u root -p${databasePassword} ${database}"`;
+ return `docker exec -i $CONTAINER_ID sh -c "mysql -u root -p'${databasePassword}' ${database}"`;
};
export const getMongoRestoreCommand = (
@@ -30,7 +30,7 @@ export const getMongoRestoreCommand = (
databaseUser: string,
databasePassword: string,
) => {
- return `docker exec -i $CONTAINER_ID sh -c "mongorestore --username ${databaseUser} --password ${databasePassword} --authenticationDatabase admin --db ${database} --archive"`;
+ return `docker exec -i $CONTAINER_ID sh -c "mongorestore --username '${databaseUser}' --password '${databasePassword}' --authenticationDatabase admin --db ${database} --archive"`;
};
export const getComposeSearchCommand = (