From 982f4d70f70c7ec1db54c3be52263da7b1fc3e47 Mon Sep 17 00:00:00 2001
From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>
Date: Wed, 22 Jul 2026 00:12:53 -0600
Subject: [PATCH 01/22] Merge pull request #4890 from SteadEXE/canary
fix(ui): add cursor pointer style for buttons
(cherry picked from commit 0fc75840d22c459e7cb7ff65d55bbb647d8d2154)
---
apps/dokploy/styles/globals.css | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/apps/dokploy/styles/globals.css b/apps/dokploy/styles/globals.css
index 50c516bd17..d212474c75 100644
--- a/apps/dokploy/styles/globals.css
+++ b/apps/dokploy/styles/globals.css
@@ -269,6 +269,12 @@
@apply bg-background text-foreground;
}
+ /* Cursor pointer on buttons */
+ button:not([disabled]),
+ [role="button"]:not([disabled]) {
+ cursor: pointer;
+ }
+
/* Custom scrollbar styling */
::-webkit-scrollbar {
width: 0.3125rem;
From b024c9c2fd6ec363e321dd8c4b39d7ca418b5936 Mon Sep 17 00:00:00 2001
From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>
Date: Wed, 22 Jul 2026 14:33:42 -0600
Subject: [PATCH 02/22] Merge pull request #4626 from
veksen/veksen/textarea-resets-while-editing
fix: prevent environment form from resetting while editing
(cherry picked from commit 73e4fdd757da90fb1fe347a92b92237e6712f98d)
---
.../components/dashboard/application/environment/show.tsx | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/apps/dokploy/components/dashboard/application/environment/show.tsx b/apps/dokploy/components/dashboard/application/environment/show.tsx
index 909cf5287c..378a871ada 100644
--- a/apps/dokploy/components/dashboard/application/environment/show.tsx
+++ b/apps/dokploy/components/dashboard/application/environment/show.tsx
@@ -60,14 +60,16 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
const currentBuildArgs = form.watch("buildArgs");
const currentBuildSecrets = form.watch("buildSecrets");
const currentCreateEnvFile = form.watch("createEnvFile");
+ const { isDirty } = form.formState;
const hasChanges =
currentEnv !== (data?.env || "") ||
currentBuildArgs !== (data?.buildArgs || "") ||
currentBuildSecrets !== (data?.buildSecrets || "") ||
currentCreateEnvFile !== (data?.createEnvFile ?? true);
+ // Skip reset while editing so background refetches don't wipe edits
useEffect(() => {
- if (data) {
+ if (data && !isDirty) {
form.reset({
env: data.env || "",
buildArgs: data.buildArgs || "",
@@ -75,7 +77,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
createEnvFile: data.createEnvFile ?? true,
});
}
- }, [data, form]);
+ }, [data, isDirty, form]);
const onSubmit = async (formData: EnvironmentSchema) => {
mutateAsync({
@@ -87,6 +89,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
})
.then(async () => {
toast.success("Environments Added");
+ form.reset(formData);
await refetch();
})
.catch(() => {
From 6e84f39a9f4603e0949eb38662121a70afabc8df Mon Sep 17 00:00:00 2001
From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>
Date: Tue, 28 Jul 2026 14:09:42 -0600
Subject: [PATCH 03/22] Merge pull request #4929 from
Dokploy/fix/remove-buttons-not-persisting
fix(ui): make interactive icons inside badges clickable again
(cherry picked from commit 425d478a0b8d8201098b3f8404f9e9c340c767b6)
---
apps/dokploy/components/ui/badge.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/apps/dokploy/components/ui/badge.tsx b/apps/dokploy/components/ui/badge.tsx
index bcd0ffe05e..ef7e697dd4 100644
--- a/apps/dokploy/components/ui/badge.tsx
+++ b/apps/dokploy/components/ui/badge.tsx
@@ -5,7 +5,7 @@ import type * as React from "react";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
- "group/badge inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2.5 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
+ "group/badge inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2.5 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg:not(.cursor-pointer)]:pointer-events-none [&>svg]:size-3!",
{
variants: {
variant: {
From c6607f3a198b393576f1892ba222297c62d54a2c Mon Sep 17 00:00:00 2001
From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>
Date: Tue, 28 Jul 2026 15:13:42 -0600
Subject: [PATCH 04/22] Merge pull request #4924 from
dmtrTm/fix/rollback-query-arg-limit
fix: avoid postgres 100-argument limit in findRollbackById
(cherry picked from commit 069939e8f99a33b0a10b1e8cd2236756acea0568)
---
packages/server/src/services/rollbacks.ts | 14 +-------------
1 file changed, 1 insertion(+), 13 deletions(-)
diff --git a/packages/server/src/services/rollbacks.ts b/packages/server/src/services/rollbacks.ts
index 97fa4b1c5f..cc93d3354d 100644
--- a/packages/server/src/services/rollbacks.ts
+++ b/packages/server/src/services/rollbacks.ts
@@ -105,19 +105,7 @@ export const findRollbackById = async (rollbackId: string) => {
const result = await db.query.rollbacks.findFirst({
where: eq(rollbacks.rollbackId, rollbackId),
with: {
- deployment: {
- with: {
- application: {
- with: {
- environment: {
- with: {
- project: true,
- },
- },
- },
- },
- },
- },
+ deployment: true,
},
});
From 43229c89da14e962adf7a0c165791088ce24dbaf Mon Sep 17 00:00:00 2001
From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>
Date: Tue, 28 Jul 2026 15:22:18 -0600
Subject: [PATCH 05/22] Merge pull request #4931 from
Dokploy/fix/postgres-100-arg-limit-finders
fix: avoid postgres 100-argument limit in schedule, volume backup and port queries
(cherry picked from commit deebf0f1078c2954fe3c29793799c7a0211fc25a)
---
apps/schedules/src/utils.ts | 6 ++++++
packages/server/src/services/port.ts | 8 ++------
packages/server/src/services/schedule.ts | 5 +++++
packages/server/src/services/volume-backups.ts | 5 +++++
4 files changed, 18 insertions(+), 6 deletions(-)
diff --git a/apps/schedules/src/utils.ts b/apps/schedules/src/utils.ts
index 7caa106afc..85de85f9a9 100644
--- a/apps/schedules/src/utils.ts
+++ b/apps/schedules/src/utils.ts
@@ -180,6 +180,9 @@ export const initializeJobs = async () => {
where: eq(schedules.enabled, true),
with: {
application: {
+ columns: {
+ applicationId: true,
+ },
with: {
server: true,
},
@@ -227,6 +230,9 @@ export const initializeJobs = async () => {
where: eq(volumeBackups.enabled, true),
with: {
application: {
+ columns: {
+ applicationId: true,
+ },
with: {
server: true,
},
diff --git a/packages/server/src/services/port.ts b/packages/server/src/services/port.ts
index 3ba1b73766..bcc5831a3a 100644
--- a/packages/server/src/services/port.ts
+++ b/packages/server/src/services/port.ts
@@ -30,12 +30,8 @@ export const finPortById = async (portId: string) => {
where: eq(ports.portId, portId),
with: {
application: {
- with: {
- environment: {
- with: {
- project: true,
- },
- },
+ columns: {
+ applicationId: true,
},
},
},
diff --git a/packages/server/src/services/schedule.ts b/packages/server/src/services/schedule.ts
index c94973e024..59b75cc0da 100644
--- a/packages/server/src/services/schedule.ts
+++ b/packages/server/src/services/schedule.ts
@@ -81,6 +81,11 @@ export const findScheduleById = async (scheduleId: string) => {
where: eq(schedules.scheduleId, scheduleId),
with: {
application: {
+ columns: {
+ applicationId: true,
+ appName: true,
+ serverId: true,
+ },
with: {
environment: {
with: {
diff --git a/packages/server/src/services/volume-backups.ts b/packages/server/src/services/volume-backups.ts
index abd29df1f4..359ccf5f0d 100644
--- a/packages/server/src/services/volume-backups.ts
+++ b/packages/server/src/services/volume-backups.ts
@@ -13,6 +13,11 @@ export const findVolumeBackupById = async (volumeBackupId: string) => {
where: eq(volumeBackups.volumeBackupId, volumeBackupId),
with: {
application: {
+ columns: {
+ applicationId: true,
+ appName: true,
+ serverId: true,
+ },
with: {
environment: {
with: {
From 7f41b098f9f6e0781f5b9087700d49202e309d80 Mon Sep 17 00:00:00 2001
From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>
Date: Tue, 28 Jul 2026 16:35:53 -0600
Subject: [PATCH 06/22] Merge pull request #4911 from
AbiRaditya/fix/cloudflare-badge-tooltip-error-title
fix(domains): don't render CDN info message as an error in DNS tooltip
(cherry picked from commit 5df820a74074cf587aee8e321f692345825e7ea3)
---
.../components/dashboard/application/domains/columns.tsx | 4 +++-
.../dashboard/application/domains/show-domains.tsx | 5 ++++-
2 files changed, 7 insertions(+), 2 deletions(-)
diff --git a/apps/dokploy/components/dashboard/application/domains/columns.tsx b/apps/dokploy/components/dashboard/application/domains/columns.tsx
index b88443dcc7..816fbbb8de 100644
--- a/apps/dokploy/components/dashboard/application/domains/columns.tsx
+++ b/apps/dokploy/components/dashboard/application/domains/columns.tsx
@@ -209,7 +209,9 @@ export const createColumns = ({
- {validationState?.error ? (
+ {validationState?.isValid && validationState?.message ? (
+ {validationState.message}
+ ) : validationState?.error ? (
Error:
{validationState.error}
diff --git a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx
index daed0e2516..23d9e46bb6 100644
--- a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx
+++ b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx
@@ -626,7 +626,10 @@ export const ShowDomains = ({ id, type }: Props) => {
- {validationState?.error ? (
+ {validationState?.isValid &&
+ validationState?.message ? (
+ {validationState.message}
+ ) : validationState?.error ? (
Error:
From d4e087e12854a1722b718c574f4e253e75a37415 Mon Sep 17 00:00:00 2001
From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>
Date: Thu, 30 Jul 2026 11:23:32 -0600
Subject: [PATCH 07/22] Merge pull request #4937 from
Dokploy/fix/persist-trigger-type-provider-forms
fix(ui): persist trigger type selection in GitHub provider forms
(cherry picked from commit bb73c6e6dd37f735b41de4452550c3e67bee93c0)
---
.../application/general/generic/save-github-provider.tsx | 8 ++++++--
.../general/generic/save-github-provider-compose.tsx | 8 ++++++--
2 files changed, 12 insertions(+), 4 deletions(-)
diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx
index 29df617b1d..3904bfc6ec 100644
--- a/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx
+++ b/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx
@@ -438,8 +438,12 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
)}
- {error && (
-
- )}
-
{
- const statusCode = res ? res.statusCode : err ? err.statusCode : 404;
- return { statusCode, error: err };
+ErrorPage.getInitialProps = ({ res, err }: NextPageContext) => {
+ const statusCode = res ? res.statusCode : err ? (err.statusCode ?? 500) : 404;
+ return { statusCode };
};
From 0f0102ff2d0962ae2bae497cb020da09cd79fe06 Mon Sep 17 00:00:00 2001
From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>
Date: Sun, 2 Aug 2026 15:36:54 -0600
Subject: [PATCH 13/22] Merge pull request #4933 from
CyrilBIENNE/fix/preview-deployment-github-credentials
fix(preview-deployment): refetch github provider before authenticating
(cherry picked from commit a058d1f5c7d0398e5fe1f9fe7bf5c3f358d559db)
---
packages/server/src/services/preview-deployment.ts | 14 ++++++++++++--
1 file changed, 12 insertions(+), 2 deletions(-)
diff --git a/packages/server/src/services/preview-deployment.ts b/packages/server/src/services/preview-deployment.ts
index 20f64259bc..cc3d3dc020 100644
--- a/packages/server/src/services/preview-deployment.ts
+++ b/packages/server/src/services/preview-deployment.ts
@@ -17,7 +17,7 @@ import { manageDomain } from "../utils/traefik/domain";
import { findApplicationById } from "./application";
import { removeDeploymentsByPreviewDeploymentId } from "./deployment";
import { createDomain } from "./domain";
-import { type Github, getIssueComment } from "./github";
+import { findGithubById, getIssueComment } from "./github";
import { getWebServerSettings } from "./web-server-settings";
export type PreviewDeployment = typeof previewDeployments.$inferSelect;
@@ -142,7 +142,17 @@ export const createPreviewDeployment = async (
org?.ownerId || "",
);
- const octokit = authGithub(application?.github as Github);
+ if (!application.githubId) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Github Account not configured correctly",
+ });
+ }
+
+ // `findApplicationById` redacts `githubPrivateKey` from the `github`
+ // relation, so the provider must be refetched to authenticate.
+ const githubProvider = await findGithubById(application.githubId);
+ const octokit = authGithub(githubProvider);
const runningComment = getIssueComment(
application.name,
From 83008205b9e26c39d5d43b75121bfdd37473bd21 Mon Sep 17 00:00:00 2001
From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>
Date: Sun, 2 Aug 2026 16:22:35 -0600
Subject: [PATCH 14/22] Merge pull request #4955 from
Dokploy/fix/schedule-run-manually-deployment-metadata
fix(schedule): return deployment metadata from runManually and fail early on missing container
(cherry picked from commit 11e93dde38ba732780a98df5f8431f0b20dcc676)
---
.../application/schedules/show-schedules.tsx | 8 +-
apps/dokploy/server/api/routers/schedule.ts | 8 +-
packages/server/src/utils/schedules/utils.ts | 142 ++++++++++--------
3 files changed, 90 insertions(+), 68 deletions(-)
diff --git a/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx b/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx
index b1e8a74f81..3c76fb969f 100644
--- a/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx
+++ b/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx
@@ -58,8 +58,12 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
const handleRunManually = async (scheduleId: string) => {
setRunningSchedules((prev) => new Set(prev).add(scheduleId));
try {
- await runManually({ scheduleId });
- toast.success("Schedule run successfully");
+ const result = await runManually({ scheduleId });
+ if (result.status === "error") {
+ toast.error("Schedule run failed, check the deployment logs");
+ } else {
+ toast.success("Schedule run successfully");
+ }
await refetchSchedules();
} catch {
toast.error("Error running schedule");
diff --git a/apps/dokploy/server/api/routers/schedule.ts b/apps/dokploy/server/api/routers/schedule.ts
index 9f0c16b1ee..c770717902 100644
--- a/apps/dokploy/server/api/routers/schedule.ts
+++ b/apps/dokploy/server/api/routers/schedule.ts
@@ -315,13 +315,17 @@ export const scheduleRouter = createTRPCRouter({
await checkPermission(ctx, { schedule: ["create"] });
}
try {
- await runCommand(input.scheduleId);
+ const deployment = await runCommand(input.scheduleId);
await audit(ctx, {
action: "run",
resourceType: "schedule",
resourceId: input.scheduleId,
});
- return true;
+ return {
+ status: deployment.status,
+ deploymentId: deployment.deploymentId,
+ logPath: deployment.logPath,
+ };
} catch (error) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
diff --git a/packages/server/src/utils/schedules/utils.ts b/packages/server/src/utils/schedules/utils.ts
index 8107839d8b..40bc0816c5 100644
--- a/packages/server/src/utils/schedules/utils.ts
+++ b/packages/server/src/utils/schedules/utils.ts
@@ -55,25 +55,46 @@ export const runCommand = async (scheduleId: string) => {
description: "Schedule",
});
- if (scheduleType === "application" || scheduleType === "compose") {
- let containerId = "";
- let serverId = "";
- if (scheduleType === "application" && application) {
- const container = await getServiceContainer(
- application.appName,
- application.serverId,
- );
- containerId = container?.Id || "";
- serverId = application.serverId || "";
- }
- if (scheduleType === "compose" && compose) {
- const container = await getComposeContainer(compose, serviceName || "");
- containerId = container?.Id || "";
- serverId = compose.serverId || "";
- }
+ try {
+ if (scheduleType === "application" || scheduleType === "compose") {
+ let containerId = "";
+ let serverId = "";
+ if (scheduleType === "application" && application) {
+ const container = await getServiceContainer(
+ application.appName,
+ application.serverId,
+ );
+ containerId = container?.Id || "";
+ serverId = application.serverId || "";
+ }
+ if (scheduleType === "compose" && compose) {
+ const container = await getComposeContainer(compose, serviceName || "");
+ containerId = container?.Id || "";
+ serverId = compose.serverId || "";
+ }
+
+ if (!containerId) {
+ const target =
+ scheduleType === "compose"
+ ? `service '${serviceName}' of compose '${compose?.name}'`
+ : `application '${application?.appName}'`;
+ const message = `Container not found for ${target}, make sure the service is running`;
+ if (serverId) {
+ await execAsyncRemote(
+ serverId,
+ `echo ${quote([`❌ ${message}`])} >> ${quote([deployment.logPath])}`,
+ );
+ } else {
+ const writeStream = createWriteStream(deployment.logPath, {
+ flags: "a",
+ });
+ writeStream.write(`❌ ${message}\n`);
+ writeStream.end();
+ }
+ throw new Error(message);
+ }
- if (serverId) {
- try {
+ if (serverId) {
await execAsyncRemote(
serverId,
`
@@ -86,47 +107,44 @@ export const runCommand = async (scheduleId: string) => {
echo "✅ Command executed successfully" >> ${quote([deployment.logPath])};
`,
);
- } catch (error) {
- await updateDeploymentStatus(deployment.deploymentId, "error");
- throw error;
- }
- } else {
- const writeStream = createWriteStream(deployment.logPath, { flags: "a" });
+ } else {
+ const writeStream = createWriteStream(deployment.logPath, {
+ flags: "a",
+ });
- try {
- if (IS_CLOUD) {
+ try {
+ if (IS_CLOUD) {
+ writeStream.write(
+ "This feature is not available in the cloud version.",
+ );
+ writeStream.end();
+ return { ...deployment, status: "running" as const };
+ }
writeStream.write(
- "This feature is not available in the cloud version.",
+ `docker exec ${containerId} ${shellType} -c ${command}\n`,
+ );
+ await spawnAsync(
+ "docker",
+ ["exec", containerId, shellType, "-c", command],
+ (data) => {
+ if (writeStream.writable) {
+ writeStream.write(data);
+ }
+ },
);
+
+ writeStream.write("✅ Command executed successfully\n");
writeStream.end();
- return;
+ } catch (error) {
+ writeStream.write("❌ Command failed\n");
+ writeStream.write(
+ error instanceof Error ? error.message : "Unknown error",
+ );
+ writeStream.end();
+ throw error;
}
- writeStream.write(
- `docker exec ${containerId} ${shellType} -c ${command}\n`,
- );
- await spawnAsync(
- "docker",
- ["exec", containerId, shellType, "-c", command],
- (data) => {
- if (writeStream.writable) {
- writeStream.write(data);
- }
- },
- );
-
- writeStream.write("✅ Command executed successfully\n");
- } catch (error) {
- writeStream.write("❌ Command failed\n");
- writeStream.write(
- error instanceof Error ? error.message : "Unknown error",
- );
- writeStream.end();
- await updateDeploymentStatus(deployment.deploymentId, "error");
- throw error;
}
- }
- } else if (scheduleType === "dokploy-server") {
- try {
+ } else if (scheduleType === "dokploy-server") {
const writeStream = createWriteStream(deployment.logPath, { flags: "a" });
const { SCHEDULES_PATH } = paths();
const fullPath = path.join(SCHEDULES_PATH, appName || "");
@@ -151,18 +169,13 @@ export const runCommand = async (scheduleId: string) => {
cwd: fullPath,
},
);
- } catch (error) {
- await updateDeploymentStatus(deployment.deploymentId, "error");
- throw error;
- }
- } else if (scheduleType === "server") {
- try {
+ } else if (scheduleType === "server") {
const { SCHEDULES_PATH } = paths(true);
const fullPath = path.join(SCHEDULES_PATH, appName || "");
const command = `
set -e
echo "Running script" >> ${deployment.logPath};
- bash -c ${fullPath}/script.sh 2>&1 | tee -a ${deployment.logPath} || {
+ bash -c ${fullPath}/script.sh 2>&1 | tee -a ${deployment.logPath} || {
echo "❌ Command failed" >> ${deployment.logPath};
exit 1;
}
@@ -177,10 +190,11 @@ export const runCommand = async (scheduleId: string) => {
});
}
});
- } catch (error) {
- await updateDeploymentStatus(deployment.deploymentId, "error");
- throw error;
}
+ await updateDeploymentStatus(deployment.deploymentId, "done");
+ return { ...deployment, status: "done" as const };
+ } catch {
+ await updateDeploymentStatus(deployment.deploymentId, "error");
+ return { ...deployment, status: "error" as const };
}
- await updateDeploymentStatus(deployment.deploymentId, "done");
};
From 13b897c01deee0f9509e0cd3e17a77804fa7ba3c Mon Sep 17 00:00:00 2001
From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>
Date: Mon, 3 Aug 2026 03:43:17 -0600
Subject: [PATCH 15/22] Merge pull request #4959 from
Dokploy/fix/cleanup-preview-deployments-on-app-delete
fix(application): remove preview deployments when deleting an application
(cherry picked from commit 056b6983db6387ece925a60661d8fcd4b45f7d30)
---
apps/dokploy/server/api/routers/application.ts | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/apps/dokploy/server/api/routers/application.ts b/apps/dokploy/server/api/routers/application.ts
index e1bbedcb9e..b6e2a3ee43 100644
--- a/apps/dokploy/server/api/routers/application.ts
+++ b/apps/dokploy/server/api/routers/application.ts
@@ -4,6 +4,7 @@ import {
deleteAllMiddlewares,
findApplicationById,
findEnvironmentById,
+ findPreviewDeploymentsByApplicationId,
findProjectById,
getAccessibleServerIds,
getApplicationStats,
@@ -16,6 +17,7 @@ import {
removeDeployments,
removeDirectoryCode,
removeMonitoringDirectory,
+ removePreviewDeployment,
removeService,
removeTraefikConfig,
startService,
@@ -234,6 +236,15 @@ export const applicationRouter = createTRPCRouter({
});
}
+ const previewDeploymentsList =
+ await findPreviewDeploymentsByApplicationId(input.applicationId);
+
+ for (const previewDeployment of previewDeploymentsList) {
+ try {
+ await removePreviewDeployment(previewDeployment.previewDeploymentId);
+ } catch (_) {}
+ }
+
const result = await db
.delete(applications)
.where(eq(applications.applicationId, input.applicationId))
From b7fdbd470417c87829450e0771d3b845d30efa81 Mon Sep 17 00:00:00 2001
From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>
Date: Mon, 3 Aug 2026 03:55:49 -0600
Subject: [PATCH 16/22] Merge pull request #4557 from
rnkp755/fix/env-update-issue
fix: invalidate railpack build cache when env changes
(cherry picked from commit a1230098c99aae40d8bab271d84ac5e6a2c54293)
---
.../__test__/deploy/railpack.command.test.ts | 104 ++++++++++++++++++
.../server/src/utils/builders/railpack.ts | 11 +-
2 files changed, 107 insertions(+), 8 deletions(-)
create mode 100644 apps/dokploy/__test__/deploy/railpack.command.test.ts
diff --git a/apps/dokploy/__test__/deploy/railpack.command.test.ts b/apps/dokploy/__test__/deploy/railpack.command.test.ts
new file mode 100644
index 0000000000..94c95c950a
--- /dev/null
+++ b/apps/dokploy/__test__/deploy/railpack.command.test.ts
@@ -0,0 +1,104 @@
+import type { ApplicationNested } from "@dokploy/server/utils/builders";
+import { getRailpackCommand } from "@dokploy/server/utils/builders/railpack";
+import { describe, expect, it } from "vitest";
+
+const createApplication = (
+ overrides: Partial
= {},
+): ApplicationNested =>
+ ({
+ appName: "test-app",
+ buildType: "railpack",
+ sourceType: "git",
+ buildPath: "/",
+ railpackVersion: "0.15.4",
+ env: "TEST_VAR=one",
+ cleanCache: false,
+ environment: {
+ project: {
+ env: "",
+ },
+ env: "",
+ },
+ ...overrides,
+ }) as unknown as ApplicationNested;
+
+const getSecretsHash = (command: string) => {
+ const match = command.match(/secrets-hash=([a-f0-9]{64})/);
+ if (!match?.[1]) {
+ throw new Error("secrets-hash build arg was not found");
+ }
+
+ return match[1];
+};
+
+describe("getRailpackCommand", () => {
+ it("includes secrets-hash without clean cache", () => {
+ const command = getRailpackCommand(createApplication());
+
+ expect(command).toContain("--build-arg secrets-hash=");
+ expect(command).not.toContain("cache-key=");
+ });
+
+ it("includes cache-key only when clean cache is enabled", () => {
+ const command = getRailpackCommand(
+ createApplication({
+ cleanCache: true,
+ }),
+ );
+
+ expect(command).toContain("--build-arg secrets-hash=");
+ expect(command).toContain("--build-arg cache-key=");
+ });
+
+ it("changes secrets-hash when an environment value changes", () => {
+ const firstCommand = getRailpackCommand(
+ createApplication({
+ env: "TEST_VAR=one",
+ }),
+ );
+ const secondCommand = getRailpackCommand(
+ createApplication({
+ env: "TEST_VAR=two",
+ }),
+ );
+
+ expect(getSecretsHash(firstCommand)).not.toEqual(
+ getSecretsHash(secondCommand),
+ );
+ });
+
+ it("changes secrets-hash when referenced project or environment values change", () => {
+ const firstCommand = getRailpackCommand(
+ createApplication({
+ env: [
+ "PROJECT_VALUE=${{project.SHARED_VALUE}}",
+ "ENVIRONMENT_VALUE=${{environment.SHARED_VALUE}}",
+ ].join("\n"),
+ environment: {
+ project: {
+ env: "SHARED_VALUE=one",
+ },
+ env: "SHARED_VALUE=alpha",
+ },
+ } as Partial),
+ );
+ const secondCommand = getRailpackCommand(
+ createApplication({
+ env: [
+ "PROJECT_VALUE=${{project.SHARED_VALUE}}",
+ "ENVIRONMENT_VALUE=${{environment.SHARED_VALUE}}",
+ ].join("\n"),
+ environment: {
+ project: {
+ env: "SHARED_VALUE=two",
+ },
+ env: "SHARED_VALUE=beta",
+ },
+ } as Partial),
+ );
+
+ expect(getSecretsHash(firstCommand)).not.toEqual(
+ getSecretsHash(secondCommand),
+ );
+ });
+});
diff --git a/packages/server/src/utils/builders/railpack.ts b/packages/server/src/utils/builders/railpack.ts
index b12296a566..b393f44603 100644
--- a/packages/server/src/utils/builders/railpack.ts
+++ b/packages/server/src/utils/builders/railpack.ts
@@ -53,14 +53,9 @@ export const getRailpackCommand = (application: ApplicationNested) => {
"build",
"--builder",
builderName,
- ...(cacheKey
- ? [
- "--build-arg",
- `secrets-hash=${secretsHash}`,
- "--build-arg",
- `cache-key=${cacheKey}`,
- ]
- : []),
+ "--build-arg",
+ `secrets-hash=${secretsHash}`,
+ ...(cacheKey ? ["--build-arg", `cache-key=${cacheKey}`] : []),
"--build-arg",
`BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v${application.railpackVersion}`,
"-f",
From 112b2c8358fd58d99058e8562b34ce2fcae9eeb0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Narciso=20E=2E=20N=C3=BA=C3=B1ez=20Arias?=
Date: Mon, 3 Aug 2026 13:41:06 -0400
Subject: [PATCH 17/22] Merge pull request #4923 from
dmtrTm/fix/rollback-environment-variables
fix: resolve environment variables on application rollback
(cherry picked from commit 46c87a22b08408b58c329b0bfe4a35511252e037)
---
.../__test__/env/rollback-environment.test.ts | 108 ++++++++++++++++++
packages/server/src/db/schema/rollbacks.ts | 3 +-
packages/server/src/services/rollbacks.ts | 4 +-
3 files changed, 113 insertions(+), 2 deletions(-)
create mode 100644 apps/dokploy/__test__/env/rollback-environment.test.ts
diff --git a/apps/dokploy/__test__/env/rollback-environment.test.ts b/apps/dokploy/__test__/env/rollback-environment.test.ts
new file mode 100644
index 0000000000..7d71059e05
--- /dev/null
+++ b/apps/dokploy/__test__/env/rollback-environment.test.ts
@@ -0,0 +1,108 @@
+import { prepareEnvironmentVariables } from "@dokploy/server/index";
+import { describe, expect, it } from "vitest";
+
+const projectEnv = `
+ENVIRONMENT=staging
+DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db
+`;
+
+const environmentEnv = `
+NODE_ENV=production
+POSTGRES_HOST=postgres.internal
+POSTGRES_PORT=5432
+REDIS_URL=redis://redis.internal:6379
+`;
+
+const serviceEnv = `
+NODE_ENV=\${{environment.NODE_ENV}}
+REDIS_URL=\${{environment.REDIS_URL}}
+PORT=3000
+`;
+
+/**
+ * A rollback replays the snapshot stored in `rollbacks.fullContext`, which keeps
+ * the service env, the environment env and the project env captured at deploy time.
+ */
+const fullContext = {
+ env: serviceEnv,
+ environment: {
+ env: environmentEnv,
+ project: {
+ env: projectEnv,
+ },
+ },
+};
+
+describe("prepareEnvironmentVariables for application rollback", () => {
+ it("resolves environment variables from the rollback snapshot", () => {
+ const result = prepareEnvironmentVariables(
+ fullContext.env,
+ fullContext.environment.project.env,
+ fullContext.environment.env,
+ );
+
+ expect(result).toEqual([
+ "NODE_ENV=production",
+ "REDIS_URL=redis://redis.internal:6379",
+ "PORT=3000",
+ ]);
+ });
+
+ it("resolves project and environment variables together on rollback", () => {
+ const rollbackEnv = `
+DATABASE_URL=\${{project.DATABASE_URL}}
+POSTGRES_URL=postgres://\${{environment.POSTGRES_HOST}}:\${{environment.POSTGRES_PORT}}/app
+ENVIRONMENT=\${{project.ENVIRONMENT}}
+`;
+
+ const result = prepareEnvironmentVariables(
+ rollbackEnv,
+ projectEnv,
+ environmentEnv,
+ );
+
+ expect(result).toEqual([
+ "DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db",
+ "POSTGRES_URL=postgres://postgres.internal:5432/app",
+ "ENVIRONMENT=staging",
+ ]);
+ });
+
+ it("throws when the environment env of the snapshot is not passed", () => {
+ expect(() =>
+ prepareEnvironmentVariables(fullContext.env, projectEnv),
+ ).toThrow("Invalid environment variable: environment.NODE_ENV");
+ });
+
+ it("maintains precedence: service > environment > project on rollback", () => {
+ const conflictingProjectEnv = `
+NODE_ENV=project
+API_URL=https://project.api.com
+`;
+
+ const conflictingEnvironmentEnv = `
+NODE_ENV=environment
+API_URL=https://environment.api.com
+`;
+
+ const rollbackEnv = `
+NODE_ENV=service
+PROJECT_API_URL=\${{project.API_URL}}
+ENVIRONMENT_API_URL=\${{environment.API_URL}}
+SELF_REFERENCE=\${{NODE_ENV}}
+`;
+
+ const result = prepareEnvironmentVariables(
+ rollbackEnv,
+ conflictingProjectEnv,
+ conflictingEnvironmentEnv,
+ );
+
+ expect(result).toEqual([
+ "NODE_ENV=service",
+ "PROJECT_API_URL=https://project.api.com",
+ "ENVIRONMENT_API_URL=https://environment.api.com",
+ "SELF_REFERENCE=service",
+ ]);
+ });
+});
diff --git a/packages/server/src/db/schema/rollbacks.ts b/packages/server/src/db/schema/rollbacks.ts
index ec27d683f8..5e3d407768 100644
--- a/packages/server/src/db/schema/rollbacks.ts
+++ b/packages/server/src/db/schema/rollbacks.ts
@@ -1,4 +1,5 @@
import type { Application } from "@dokploy/server/services/application";
+import type { Environment } from "@dokploy/server/services/environment";
import type { Mount } from "@dokploy/server/services/mount";
import type { Port } from "@dokploy/server/services/port";
import type { Project } from "@dokploy/server/services/project";
@@ -27,7 +28,7 @@ export const rollbacks = pgTable("rollback", {
.$defaultFn(() => new Date().toISOString()),
fullContext: jsonb("fullContext").$type<
Application & {
- environment: {
+ environment: Environment & {
project: Project;
};
mounts: Mount[];
diff --git a/packages/server/src/services/rollbacks.ts b/packages/server/src/services/rollbacks.ts
index cc93d3354d..b3ccc5a5f8 100644
--- a/packages/server/src/services/rollbacks.ts
+++ b/packages/server/src/services/rollbacks.ts
@@ -19,6 +19,7 @@ import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
import { getRemoteDocker } from "../utils/servers/remote-docker";
import { type Application, findApplicationById } from "./application";
import { findDeploymentById } from "./deployment";
+import type { Environment } from "./environment";
import type { Mount } from "./mount";
import type { Port } from "./port";
import type { Project } from "./project";
@@ -201,7 +202,7 @@ const rollbackApplication = async (
image: string,
serverId?: string | null,
fullContext?: Application & {
- environment: {
+ environment: Environment & {
project: Project;
};
mounts: Mount[];
@@ -263,6 +264,7 @@ const rollbackApplication = async (
const envVariables = prepareEnvironmentVariables(
env,
fullContext.environment.project.env,
+ fullContext.environment.env,
);
let rollbackImage = image;
From 3e4ed0c299f110f0bf951e1f1b4a9fbacbe9d425 Mon Sep 17 00:00:00 2001
From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>
Date: Tue, 4 Aug 2026 10:59:35 -0600
Subject: [PATCH 18/22] Merge pull request #4966 from
Dokploy/fix/requests-hostname-filter-crash
fix(requests): guard RequestHost before filtering to avoid crash on malformed logs
(cherry picked from commit a0162ab5669badc9a8eb1aa45b44812a2bfda480)
---
.../dokploy/__test__/requests/request.test.ts | 22 +++++++++++++++++++
packages/server/src/utils/access-log/utils.ts | 2 +-
2 files changed, 23 insertions(+), 1 deletion(-)
diff --git a/apps/dokploy/__test__/requests/request.test.ts b/apps/dokploy/__test__/requests/request.test.ts
index 3f58ac439a..844458fd24 100644
--- a/apps/dokploy/__test__/requests/request.test.ts
+++ b/apps/dokploy/__test__/requests/request.test.ts
@@ -55,6 +55,28 @@ describe("processLogs", () => {
expect(result.data).toHaveLength(2);
});
+ it("should not throw when filtering by hostname and an entry has no RequestHost", () => {
+ const entryWithoutRequestHost = sampleLogEntry.replace(
+ /"RequestHost":"[^"]*",/,
+ "",
+ );
+
+ const mixedEntries = `${sampleLogEntry}\n${entryWithoutRequestHost}`;
+
+ expect(() =>
+ parseRawConfig(mixedEntries, undefined, undefined, "traefik.me"),
+ ).not.toThrow();
+
+ const result = parseRawConfig(
+ mixedEntries,
+ undefined,
+ undefined,
+ "traefik.me",
+ );
+ expect(result.totalCount).toBe(1);
+ expect(result.data[0]?.RequestHost).toBe("s222-umami-c381af.traefik.me");
+ });
+
it("should filter out Dokploy dashboard requests", () => {
const dokployDashboardEntry = `{"ClientAddr":"172.71.187.131:9485","ClientHost":"172.71.187.131","ClientPort":"9485","ClientUsername":"-","DownstreamContentSize":14550,"DownstreamStatus":200,"Duration":57681682,"OriginContentSize":14550,"OriginDuration":57612242,"OriginStatus":200,"Overhead":69440,"RequestAddr":"hostinger.dokploy.com","RequestContentSize":0,"RequestCount":20142,"RequestHost":"hostinger.dokploy.com","RequestMethod":"GET","RequestPath":"/_next/data/cb_zzI4Rp9G7Q7djrFKh0/en/dashboard/traefik.json","RequestPort":"-","RequestProtocol":"HTTP/2.0","RequestScheme":"https","RetryAttempts":0,"RouterName":"dokploy-router-app-secure@file","ServiceAddr":"dokploy:3000","ServiceName":"dokploy-service-app@file","ServiceURL":"http://dokploy:3000","StartLocal":"2025-12-10T05:10:41.957755949Z","StartUTC":"2025-12-10T05:10:41.957755949Z","TLSCipher":"TLS_AES_128_GCM_SHA256","TLSVersion":"1.3","entryPointName":"websecure","level":"info","msg":"","time":"2025-12-10T05:10:42Z"}`;
diff --git a/packages/server/src/utils/access-log/utils.ts b/packages/server/src/utils/access-log/utils.ts
index 9b472ee27d..ddaef3ec42 100644
--- a/packages/server/src/utils/access-log/utils.ts
+++ b/packages/server/src/utils/access-log/utils.ts
@@ -120,7 +120,7 @@ export function parseRawConfig(
if (search) {
parsedLogs = parsedLogs.filter((log) =>
- log.RequestHost.toLowerCase().includes(search.toLowerCase()),
+ (log.RequestHost ?? "").toLowerCase().includes(search.toLowerCase()),
);
}
From e19d7bbf0e894c25ba1caf447afef1ee222311f4 Mon Sep 17 00:00:00 2001
From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>
Date: Tue, 4 Aug 2026 11:51:24 -0600
Subject: [PATCH 19/22] Merge pull request #4971 from
Dokploy/fix/restore-dialog-width
fix(ui): widen restore backup dialog to match other backup dialogs
(cherry picked from commit 3495fc89bf0edcda16ac405c537ab8541e799d35)
---
.../components/dashboard/database/backups/restore-backup.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx b/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx
index 53f5ae03e1..0c13bddb71 100644
--- a/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx
+++ b/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx
@@ -316,7 +316,7 @@ export const RestoreBackup = ({
Restore Backup
-
+
From bcf97f92aba99406b5c4471962ce23b82fba18ca Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Narciso=20E=2E=20N=C3=BA=C3=B1ez=20Arias?=
Date: Wed, 5 Aug 2026 22:21:12 -0400
Subject: [PATCH 20/22] Merge pull request #4772 from
rifatdinc/fix-watch-paths-added-removed-files
fix(webhook): include added and removed files in watchPaths validation
(cherry picked from commit 3f98208f03072bf076a36b0c495c386142aab31b)
---
.../pages/api/deploy/[refreshToken].ts | 56 ++++++++++++-------
.../api/deploy/compose/[refreshToken].ts | 48 ++++++++++------
apps/dokploy/pages/api/deploy/github.ts | 8 ++-
3 files changed, 70 insertions(+), 42 deletions(-)
diff --git a/apps/dokploy/pages/api/deploy/[refreshToken].ts b/apps/dokploy/pages/api/deploy/[refreshToken].ts
index bb6eb06d37..9cf6142d25 100644
--- a/apps/dokploy/pages/api/deploy/[refreshToken].ts
+++ b/apps/dokploy/pages/api/deploy/[refreshToken].ts
@@ -119,9 +119,11 @@ export default async function handler(
}
// If webhook doesn't provide image info, we'll use the configured image (old behavior)
} else if (sourceType === "github") {
- const normalizedCommits = req.body?.commits?.flatMap(
- (commit: any) => commit.modified,
- );
+ const normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
+ ...(commit.added || []),
+ ...(commit.modified || []),
+ ...(commit.removed || []),
+ ]);
const shouldDeployPaths = shouldDeploy(
application.watchPaths,
@@ -150,21 +152,29 @@ export default async function handler(
let normalizedCommits: string[] = [];
if (provider === "github") {
- normalizedCommits = req.body?.commits?.flatMap(
- (commit: any) => commit.modified,
- );
+ normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
+ ...(commit.added || []),
+ ...(commit.modified || []),
+ ...(commit.removed || []),
+ ]);
} else if (provider === "gitlab") {
- normalizedCommits = req.body?.commits?.flatMap(
- (commit: any) => commit.modified,
- );
+ normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
+ ...(commit.added || []),
+ ...(commit.modified || []),
+ ...(commit.removed || []),
+ ]);
} else if (provider === "gitea") {
- normalizedCommits = req.body?.commits?.flatMap(
- (commit: any) => commit.modified,
- );
+ normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
+ ...(commit.added || []),
+ ...(commit.modified || []),
+ ...(commit.removed || []),
+ ]);
} else if (provider === "soft-serve") {
- normalizedCommits = req.body?.commits?.flatMap(
- (commit: any) => commit.modified,
- );
+ normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
+ ...(commit.added || []),
+ ...(commit.modified || []),
+ ...(commit.removed || []),
+ ]);
}
const shouldDeployPaths = shouldDeploy(
@@ -179,9 +189,11 @@ export default async function handler(
} else if (sourceType === "gitlab") {
const branchName = extractBranchName(req.headers, req.body);
- const normalizedCommits = req.body?.commits?.flatMap(
- (commit: any) => commit.modified,
- );
+ const normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
+ ...(commit.added || []),
+ ...(commit.modified || []),
+ ...(commit.removed || []),
+ ]);
const shouldDeployPaths = shouldDeploy(
application.watchPaths,
@@ -225,9 +237,11 @@ export default async function handler(
} else if (sourceType === "gitea") {
const branchName = extractBranchName(req.headers, req.body);
- const normalizedCommits = req.body?.commits?.flatMap(
- (commit: any) => commit.modified,
- );
+ const normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
+ ...(commit.added || []),
+ ...(commit.modified || []),
+ ...(commit.removed || []),
+ ]);
const shouldDeployPaths = shouldDeploy(
application.watchPaths,
diff --git a/apps/dokploy/pages/api/deploy/compose/[refreshToken].ts b/apps/dokploy/pages/api/deploy/compose/[refreshToken].ts
index 85a379eb3e..0d83c10174 100644
--- a/apps/dokploy/pages/api/deploy/compose/[refreshToken].ts
+++ b/apps/dokploy/pages/api/deploy/compose/[refreshToken].ts
@@ -54,9 +54,11 @@ export default async function handler(
if (sourceType === "github") {
const branchName = extractBranchName(req.headers, req.body);
- const normalizedCommits = req.body?.commits?.flatMap(
- (commit: any) => commit.modified,
- );
+ const normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
+ ...(commit.added || []),
+ ...(commit.modified || []),
+ ...(commit.removed || []),
+ ]);
const shouldDeployPaths = shouldDeploy(
composeResult.watchPaths,
@@ -74,9 +76,11 @@ export default async function handler(
}
} else if (sourceType === "gitlab") {
const branchName = extractBranchName(req.headers, req.body);
- const normalizedCommits = req.body?.commits?.flatMap(
- (commit: any) => commit.modified,
- );
+ const normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
+ ...(commit.added || []),
+ ...(commit.modified || []),
+ ...(commit.removed || []),
+ ]);
const shouldDeployPaths = shouldDeploy(
composeResult.watchPaths,
@@ -125,17 +129,23 @@ export default async function handler(
let normalizedCommits: string[] = [];
if (provider === "github") {
- normalizedCommits = req.body?.commits?.flatMap(
- (commit: any) => commit.modified,
- );
+ normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
+ ...(commit.added || []),
+ ...(commit.modified || []),
+ ...(commit.removed || []),
+ ]);
} else if (provider === "gitlab") {
- normalizedCommits = req.body?.commits?.flatMap(
- (commit: any) => commit.modified,
- );
+ normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
+ ...(commit.added || []),
+ ...(commit.modified || []),
+ ...(commit.removed || []),
+ ]);
} else if (provider === "gitea") {
- normalizedCommits = req.body?.commits?.flatMap(
- (commit: any) => commit.modified,
- );
+ normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
+ ...(commit.added || []),
+ ...(commit.modified || []),
+ ...(commit.removed || []),
+ ]);
}
const shouldDeployPaths = shouldDeploy(
@@ -150,9 +160,11 @@ export default async function handler(
} else if (sourceType === "gitea") {
const branchName = extractBranchName(req.headers, req.body);
- const normalizedCommits = req.body?.commits?.flatMap(
- (commit: any) => commit.modified,
- );
+ const normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
+ ...(commit.added || []),
+ ...(commit.modified || []),
+ ...(commit.removed || []),
+ ]);
const shouldDeployPaths = shouldDeploy(
composeResult.watchPaths,
diff --git a/apps/dokploy/pages/api/deploy/github.ts b/apps/dokploy/pages/api/deploy/github.ts
index f03bf786ec..4fc75fc50c 100644
--- a/apps/dokploy/pages/api/deploy/github.ts
+++ b/apps/dokploy/pages/api/deploy/github.ts
@@ -223,9 +223,11 @@ export default async function handler(
const deploymentTitle = extractCommitMessage(req.headers, req.body);
const deploymentHash = extractHash(req.headers, req.body);
const owner = getGithubRepositoryOwner(githubBody);
- const normalizedCommits = githubBody?.commits?.flatMap(
- (commit: any) => commit.modified,
- );
+ const normalizedCommits = githubBody?.commits?.flatMap((commit: any) => [
+ ...(commit.added || []),
+ ...(commit.modified || []),
+ ...(commit.removed || []),
+ ]);
const apps = await db.query.applications.findMany({
where: and(
From dd542994a4ecb15f20fa765728be1d7c96df9ec3 Mon Sep 17 00:00:00 2001
From: Mauricio Siu
Date: Wed, 5 Aug 2026 23:49:59 -0600
Subject: [PATCH 21/22] chore: release v0.29.14
---
apps/dokploy/package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json
index f6b4aeb2d5..cca13dea74 100644
--- a/apps/dokploy/package.json
+++ b/apps/dokploy/package.json
@@ -1,6 +1,6 @@
{
"name": "dokploy",
- "version": "v0.29.13",
+ "version": "v0.29.14",
"private": true,
"license": "Apache-2.0",
"type": "module",
From 57fae7385389351ba0791d3fea928159ef7dd2c8 Mon Sep 17 00:00:00 2001
From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>
Date: Wed, 5 Aug 2026 23:54:32 -0600
Subject: [PATCH 22/22] Update packages/server/src/utils/schedules/utils.ts
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
---
packages/server/src/utils/schedules/utils.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/packages/server/src/utils/schedules/utils.ts b/packages/server/src/utils/schedules/utils.ts
index 40bc0816c5..e81f9a5823 100644
--- a/packages/server/src/utils/schedules/utils.ts
+++ b/packages/server/src/utils/schedules/utils.ts
@@ -118,7 +118,8 @@ export const runCommand = async (scheduleId: string) => {
"This feature is not available in the cloud version.",
);
writeStream.end();
- return { ...deployment, status: "running" as const };
+ await updateDeploymentStatus(deployment.deploymentId, "error");
+ return { ...deployment, status: "error" as const };
}
writeStream.write(
`docker exec ${containerId} ${shellType} -c ${command}\n`,