From 43f9f545c684e067e367ad4d07f0307734291ca4 Mon Sep 17 00:00:00 2001 From: Yandi Date: Sun, 22 Feb 2026 19:56:29 +0700 Subject: [PATCH 01/11] fix: use lazy initializer for useState with function calls Calling getTerminalKey() and getDefaultDateRange() directly in useState re-runs them on every render. Wrap in an arrow function so they execute only once during component initialization. --- apps/dokploy/components/dashboard/requests/show-requests.tsx | 2 +- .../dashboard/settings/web-server/terminal-modal.tsx | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/dokploy/components/dashboard/requests/show-requests.tsx b/apps/dokploy/components/dashboard/requests/show-requests.tsx index cc4f1764a..076ed33cc 100644 --- a/apps/dokploy/components/dashboard/requests/show-requests.tsx +++ b/apps/dokploy/components/dashboard/requests/show-requests.tsx @@ -63,7 +63,7 @@ export const ShowRequests = () => { const [dateRange, setDateRange] = useState<{ from: Date | undefined; to: Date | undefined; - }>(getDefaultDateRange()); + }>(() => getDefaultDateRange()); // Check if logs exist to determine if traefik has been reloaded // Only fetch when active to minimize network calls diff --git a/apps/dokploy/components/dashboard/settings/web-server/terminal-modal.tsx b/apps/dokploy/components/dashboard/settings/web-server/terminal-modal.tsx index 2647e1dc0..e90ba51f8 100644 --- a/apps/dokploy/components/dashboard/settings/web-server/terminal-modal.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server/terminal-modal.tsx @@ -32,7 +32,9 @@ export const TerminalModal = ({ serverId, asButton = false, }: Props) => { - const [terminalKey, setTerminalKey] = useState(getTerminalKey()); + const [terminalKey, setTerminalKey] = useState(() => + getTerminalKey(), + ); const [isOpen, setIsOpen] = useState(false); const isLocalServer = serverId === "local"; From eb80f79862b751029de4ba3a4fe655003926850a Mon Sep 17 00:00:00 2001 From: Gabriel Vinhaes Date: Wed, 1 Apr 2026 16:37:56 -0300 Subject: [PATCH 02/11] fix: preserve server schedule failures through tee pipeline --- packages/server/src/utils/schedules/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/utils/schedules/utils.ts b/packages/server/src/utils/schedules/utils.ts index 64657c9a6..3330acb52 100644 --- a/packages/server/src/utils/schedules/utils.ts +++ b/packages/server/src/utils/schedules/utils.ts @@ -159,7 +159,7 @@ export const runCommand = async (scheduleId: string) => { const { SCHEDULES_PATH } = paths(true); const fullPath = path.join(SCHEDULES_PATH, appName || ""); const command = ` - set -e + set -euo pipefail echo "Running script" >> ${deployment.logPath}; bash -c ${fullPath}/script.sh 2>&1 | tee -a ${deployment.logPath} || { echo "❌ Command failed" >> ${deployment.logPath}; From f21931da60fcd5e9f7116a6bde1d39e2dc55b5b5 Mon Sep 17 00:00:00 2001 From: Daniel Abramov Date: Wed, 8 Apr 2026 18:29:29 +0200 Subject: [PATCH 03/11] fix: add unzip to arch server set up --- packages/server/src/setup/server-setup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/setup/server-setup.ts b/packages/server/src/setup/server-setup.ts index e2d7fc3e5..620a4e62b 100644 --- a/packages/server/src/setup/server-setup.ts +++ b/packages/server/src/setup/server-setup.ts @@ -466,7 +466,7 @@ const installUtilities = () => ` case "$OS_TYPE" in arch) - $SUDO_CMD pacman -Sy --noconfirm --needed curl wget git git-lfs jq openssl >/dev/null || true + $SUDO_CMD pacman -Sy --noconfirm --needed unzip curl wget git git-lfs jq openssl >/dev/null || true ;; alpine) $SUDO_CMD sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories From dc2fc40c3810e7fa7f2e684a3ecda28a9a265aa0 Mon Sep 17 00:00:00 2001 From: fcsouza Date: Fri, 12 Jun 2026 10:31:57 -0300 Subject: [PATCH 04/11] fix(logs): respect log level explicitly declared by structured loggers --- apps/dokploy/__test__/utils/log-type.test.ts | 108 ++++++++++++++++++ .../components/dashboard/docker/logs/utils.ts | 60 ++++++++++ 2 files changed, 168 insertions(+) create mode 100644 apps/dokploy/__test__/utils/log-type.test.ts diff --git a/apps/dokploy/__test__/utils/log-type.test.ts b/apps/dokploy/__test__/utils/log-type.test.ts new file mode 100644 index 000000000..b2edfd58c --- /dev/null +++ b/apps/dokploy/__test__/utils/log-type.test.ts @@ -0,0 +1,108 @@ +import { describe, expect, test } from "vitest"; +import { getLogType } from "@/components/dashboard/docker/logs/utils"; + +describe("getLogType", () => { + describe("explicit level declared by structured loggers", () => { + test("JSON string levels (pino, winston, zap)", () => { + expect(getLogType('{"level":"trace","msg":"x"}').type).toBe("debug"); + expect(getLogType('{"level":"debug","msg":"x"}').type).toBe("debug"); + expect(getLogType('{"level":"info","msg":"x"}').type).toBe("info"); + expect(getLogType('{"level":"warn","msg":"x"}').type).toBe("warning"); + expect(getLogType('{"level":"warning","msg":"x"}').type).toBe("warning"); + expect(getLogType('{"level":"error","msg":"x"}').type).toBe("error"); + expect(getLogType('{"level":"fatal","msg":"x"}').type).toBe("error"); + }); + + test("JSON numeric levels (pino, bunyan)", () => { + expect(getLogType('{"level":20,"msg":"x"}').type).toBe("debug"); + expect(getLogType('{"level":30,"msg":"x"}').type).toBe("info"); + expect(getLogType('{"level":40,"msg":"x"}').type).toBe("warning"); + expect(getLogType('{"level":50,"msg":"x"}').type).toBe("error"); + expect(getLogType('{"level":60,"msg":"x"}').type).toBe("error"); + }); + + test("syslog/GELF numeric levels", () => { + expect(getLogType('{"level":3,"msg":"x"}').type).toBe("error"); + expect(getLogType('{"level":4,"msg":"x"}').type).toBe("warning"); + expect(getLogType('{"level":6,"msg":"x"}').type).toBe("info"); + expect(getLogType('{"level":7,"msg":"x"}').type).toBe("debug"); + }); + + test("GCP-style severity and ECS log.level", () => { + expect(getLogType('{"severity":"ERROR","message":"x"}').type).toBe( + "error", + ); + expect(getLogType('{"severity":"WARNING","message":"x"}').type).toBe( + "warning", + ); + expect(getLogType('{"log.level":"error","message":"x"}').type).toBe( + "error", + ); + }); + + test("logfmt levels", () => { + expect( + getLogType('ts=2026-06-12T10:00:00Z level=error msg="boom"').type, + ).toBe("error"); + expect( + getLogType('ts=2026-06-12T10:00:00Z level=info msg="ok"').type, + ).toBe("info"); + expect(getLogType("level=warn msg=careful").type).toBe("warning"); + }); + + test("declared level wins over keywords in the message (#4589, #1996)", () => { + // "version"/"GET" in this pino line would otherwise match the debug keywords + const pinoError = + '{"level":"error","version":"72b4450","method":"GET","path":"/api/campaigns","err":{"type":"ForbiddenError","stack":"ForbiddenError: at requireRole (/app/src/plugins/campaign.plugin.ts:166:15)"},"msg":"Forbidden"}'; + expect(getLogType(pinoError).type).toBe("error"); + + // info line containing error-like keywords (#4589) + expect( + getLogType( + '{"level":"info","msg":"Failed to open mempool file. Continuing anyway."}', + ).type, + ).toBe("info"); + + // successful job summary with "failed: false, error: none" (#4538) + expect( + getLogType( + 'level=info msg="Finished job, failed: false, skipped: false, error: none"', + ).type, + ).toBe("info"); + }); + + test("declared level wins over statusCode", () => { + expect(getLogType('{"level":"info","statusCode":500}').type).toBe("info"); + }); + + test("unknown level names fall back to keyword detection", () => { + expect( + getLogType('{"level":"verbose","msg":"connection failed"}').type, + ).toBe("error"); + }); + + test("env-var-like text is not treated as logfmt level", () => { + expect(getLogType("LOG_LEVEL=error NODE_ENV=production").type).not.toBe( + "error", + ); + }); + }); + + describe("fallback detection for unstructured logs (unchanged)", () => { + test("statusCode classification", () => { + expect(getLogType('{"statusCode":500,"msg":"x"}').type).toBe("error"); + expect(getLogType('{"statusCode":404,"msg":"x"}').type).toBe("warning"); + expect(getLogType('{"statusCode":200,"msg":"x"}').type).toBe("success"); + }); + + test("keyword classification", () => { + expect(getLogType("error: something broke").type).toBe("error"); + expect(getLogType("warning: disk almost full").type).toBe("warning"); + expect(getLogType("Server listening on port 8080").type).toBe("success"); + }); + + test("defaults to info", () => { + expect(getLogType("hello world").type).toBe("info"); + }); + }); +}); diff --git a/apps/dokploy/components/dashboard/docker/logs/utils.ts b/apps/dokploy/components/dashboard/docker/logs/utils.ts index 01c68e49a..3e4695905 100644 --- a/apps/dokploy/components/dashboard/docker/logs/utils.ts +++ b/apps/dokploy/components/dashboard/docker/logs/utils.ts @@ -72,8 +72,68 @@ export function parseLogs(logString: string): LogLine[] { .filter((log) => log !== null); } +const LEVEL_NAME_TO_TYPE: Record = { + trace: "debug", + debug: "debug", + info: "info", + information: "info", + notice: "info", + warn: "warning", + warning: "warning", + error: "error", + err: "error", + fatal: "error", + critical: "error", + panic: "error", + alert: "error", + emergency: "error", +}; + +const numericLevelToType = (level: number): LogType => { + // pino/bunyan scale: 10=trace 20=debug 30=info 40=warn 50=error 60=fatal + if (level >= 50) return "error"; + if (level >= 40) return "warning"; + if (level >= 30) return "info"; + if (level >= 10) return "debug"; + // syslog/GELF scale: 0=emergency ... 7=debug + if (level <= 3) return "error"; + if (level === 4) return "warning"; + if (level <= 6) return "info"; + return "debug"; +}; + +// Extract the log level explicitly declared by structured loggers +// (pino, bunyan, winston, zap, slog, logfmt, GCP severity) +const getExplicitLevelType = (message: string): LogType | null => { + // JSON string levels: {"level":"error"} / {"severity":"ERROR"} / {"log.level":"warn"} + const jsonStringMatch = message.match( + /"(?:level|severity|log\.level|loglevel)"\s*:\s*"([a-z]+)"/i, + ); + if (jsonStringMatch?.[1]) { + return LEVEL_NAME_TO_TYPE[jsonStringMatch[1].toLowerCase()] ?? null; + } + + // JSON numeric levels: {"level":50} + const jsonNumericMatch = message.match(/"level"\s*:\s*(\d{1,2})\b/); + if (jsonNumericMatch?.[1]) { + return numericLevelToType(Number(jsonNumericMatch[1])); + } + + // logfmt: level=error + const logfmtMatch = message.match(/(?:^|\s)(?:level|severity)=([a-z]+)\b/i); + if (logfmtMatch?.[1]) { + return LEVEL_NAME_TO_TYPE[logfmtMatch[1].toLowerCase()] ?? null; + } + + return null; +}; + // Detect log type based on message content export const getLogType = (message: string): LogStyle => { + // A level explicitly declared by the logger wins over any inference + const explicitType = getExplicitLevelType(message); + if (explicitType) return LOG_STYLES[explicitType]; + // Detect HTTP statusCode const statusMatch = message.match(/"statusCode"\s*:\s*"?(\d{3})"?/); From faf600bd88e1293cbe73f91138de707bc7746fa1 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 14 Jul 2026 11:37:21 -0600 Subject: [PATCH 05/11] fix(logs): don't classify explicit non-error key/values as error Lines like 'failed: false, error: none' (e.g. ofelia job summaries for successful runs) were rendered red because the error patterns matched the key regardless of its value. Strip key/value pairs whose value explicitly reports a non-error before running the error keyword patterns. Fixes #4538 --- .../__test__/logs/log-classification.test.ts | 33 +++++++++++++++++++ .../components/dashboard/docker/logs/utils.ts | 26 +++++++++------ 2 files changed, 49 insertions(+), 10 deletions(-) create mode 100644 apps/dokploy/__test__/logs/log-classification.test.ts diff --git a/apps/dokploy/__test__/logs/log-classification.test.ts b/apps/dokploy/__test__/logs/log-classification.test.ts new file mode 100644 index 000000000..92689558a --- /dev/null +++ b/apps/dokploy/__test__/logs/log-classification.test.ts @@ -0,0 +1,33 @@ +import { getLogType } from "@/components/dashboard/docker/logs/utils"; +import { expect, test } from "vitest"; + +test("classifies real failures as error", () => { + expect(getLogType("Error: connection refused at db:5432").type).toBe( + "error", + ); + expect(getLogType("[ERROR] something went wrong").type).toBe("error"); + expect(getLogType("Deployment failed").type).toBe("error"); + expect( + getLogType( + 'NOTICE [Job "sync-1m" (e1305c5b54b1)] Finished in "326ms", failed: true, skipped: false, error: exit code 1', + ).type, + ).toBe("error"); +}); + +test("does not classify explicit non-error key/values as error (#4538)", () => { + // ofelia job-completion summary for a successful run + expect( + getLogType( + 'NOTICE [Job "sync-1m" (e1305c5b54b1)] Finished in "326.16795ms", failed: false, skipped: false, error: none', + ).type, + ).not.toBe("error"); + + expect(getLogType("request done, error: null").type).not.toBe("error"); + expect(getLogType("checks passed, failures=0").type).not.toBe("error"); + expect(getLogType('shutdown clean, error=""').type).not.toBe("error"); +}); + +test("keeps statusCode-based classification", () => { + expect(getLogType('{"statusCode": "500"}').type).toBe("error"); + expect(getLogType('{"statusCode": "204"}').type).toBe("success"); +}); diff --git a/apps/dokploy/components/dashboard/docker/logs/utils.ts b/apps/dokploy/components/dashboard/docker/logs/utils.ts index 01c68e49a..1624a3828 100644 --- a/apps/dokploy/components/dashboard/docker/logs/utils.ts +++ b/apps/dokploy/components/dashboard/docker/logs/utils.ts @@ -97,17 +97,23 @@ export const getLogType = (message: string): LogStyle => { return LOG_STYLES.info; } + // Key/value pairs that explicitly report a non-error (e.g. "error: none", + // "failed: false") must not trigger the error keyword patterns below + const nonErrorKeyValues = + /\b(?:error|err|errors|failed|failure|failures)s?\s*[:=]\s*(?:none|null|nil|false|0|no|-|""|'')(?=[\s,;.)\]]|$)/gi; + const errorScope = lowerMessage.replace(nonErrorKeyValues, ""); + if ( - /(?:^|\s)(?:error|err):?\s/i.test(lowerMessage) || - /\b(?:exception|failed|failure)\b/i.test(lowerMessage) || - /(?:stack\s?trace):\s*$/i.test(lowerMessage) || - /^\s*at\s+[\w.]+\s*\(?.+:\d+:\d+\)?/.test(lowerMessage) || - /\b(?:uncaught|unhandled)\s+(?:exception|error)\b/i.test(lowerMessage) || - /Error:\s.*(?:in|at)\s+.*:\d+(?::\d+)?/.test(lowerMessage) || - /\b(?:errno|code):\s*(?:\d+|[A-Z_]+)\b/i.test(lowerMessage) || - /\[(?:error|err|fatal)\]/i.test(lowerMessage) || - /\b(?:crash|critical|fatal)\b/i.test(lowerMessage) || - /\b(?:fail(?:ed|ure)?|broken|dead)\b/i.test(lowerMessage) + /(?:^|\s)(?:error|err):?\s/i.test(errorScope) || + /\b(?:exception|failed|failure)\b/i.test(errorScope) || + /(?:stack\s?trace):\s*$/i.test(errorScope) || + /^\s*at\s+[\w.]+\s*\(?.+:\d+:\d+\)?/.test(errorScope) || + /\b(?:uncaught|unhandled)\s+(?:exception|error)\b/i.test(errorScope) || + /Error:\s.*(?:in|at)\s+.*:\d+(?::\d+)?/.test(errorScope) || + /\b(?:errno|code):\s*(?:\d+|[A-Z_]+)\b/i.test(errorScope) || + /\[(?:error|err|fatal)\]/i.test(errorScope) || + /\b(?:crash|critical|fatal)\b/i.test(errorScope) || + /\b(?:fail(?:ed|ure)?|broken|dead)\b/i.test(errorScope) ) { return LOG_STYLES.error; } From c9817da45b57da72f520c3bd4ae14cc0ec8fe993 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 17:37:53 +0000 Subject: [PATCH 06/11] [autofix.ci] apply automated fixes --- apps/dokploy/__test__/logs/log-classification.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/dokploy/__test__/logs/log-classification.test.ts b/apps/dokploy/__test__/logs/log-classification.test.ts index 92689558a..6f04f491a 100644 --- a/apps/dokploy/__test__/logs/log-classification.test.ts +++ b/apps/dokploy/__test__/logs/log-classification.test.ts @@ -2,9 +2,7 @@ import { getLogType } from "@/components/dashboard/docker/logs/utils"; import { expect, test } from "vitest"; test("classifies real failures as error", () => { - expect(getLogType("Error: connection refused at db:5432").type).toBe( - "error", - ); + expect(getLogType("Error: connection refused at db:5432").type).toBe("error"); expect(getLogType("[ERROR] something went wrong").type).toBe("error"); expect(getLogType("Deployment failed").type).toBe("error"); expect( From 60112d84a5792716b183b3aba2d2840cbdb54134 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Mon, 3 Aug 2026 04:24:02 -0600 Subject: [PATCH 07/11] fix(logs): require complete no-error values in colon pairs --- apps/dokploy/__test__/logs/log-classification.test.ts | 11 +++++++++++ .../dokploy/components/dashboard/docker/logs/utils.ts | 10 +++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/apps/dokploy/__test__/logs/log-classification.test.ts b/apps/dokploy/__test__/logs/log-classification.test.ts index 6f04f491a..d97d76a29 100644 --- a/apps/dokploy/__test__/logs/log-classification.test.ts +++ b/apps/dokploy/__test__/logs/log-classification.test.ts @@ -23,6 +23,17 @@ test("does not classify explicit non-error key/values as error (#4538)", () => { expect(getLogType("request done, error: null").type).not.toBe("error"); expect(getLogType("checks passed, failures=0").type).not.toBe("error"); expect(getLogType('shutdown clean, error=""').type).not.toBe("error"); + expect(getLogType('job done failed=false error=""').type).not.toBe("error"); +}); + +test("keeps errors whose value merely starts with a no-error word", () => { + expect(getLogType("connect failed: no route to host").type).toBe("error"); + expect(getLogType("connection failed: no such host").type).toBe("error"); + expect(getLogType("error: none of the configured nodes are available").type).toBe( + "error", + ); + expect(getLogType("error: nil pointer dereference").type).toBe("error"); + expect(getLogType("request failed: 0 bytes received").type).toBe("error"); }); test("keeps statusCode-based classification", () => { diff --git a/apps/dokploy/components/dashboard/docker/logs/utils.ts b/apps/dokploy/components/dashboard/docker/logs/utils.ts index 1624a3828..2973b78b7 100644 --- a/apps/dokploy/components/dashboard/docker/logs/utils.ts +++ b/apps/dokploy/components/dashboard/docker/logs/utils.ts @@ -99,9 +99,13 @@ export const getLogType = (message: string): LogStyle => { // Key/value pairs that explicitly report a non-error (e.g. "error: none", // "failed: false") must not trigger the error keyword patterns below - const nonErrorKeyValues = - /\b(?:error|err|errors|failed|failure|failures)s?\s*[:=]\s*(?:none|null|nil|false|0|no|-|""|'')(?=[\s,;.)\]]|$)/gi; - const errorScope = lowerMessage.replace(nonErrorKeyValues, ""); + const nonErrorColonPairs = + /\b(?:error|err|errors|failed|failure|failures)\s*:\s*(?:none|null|nil|false|0|no|-|""|'')(?=[,;.)\]]|$)/gi; + const nonErrorLogfmtPairs = + /\b(?:error|err|errors|failed|failure|failures)\s*=\s*(?:none|null|nil|false|0|no|-|""|'')(?=[\s,;.)\]]|$)/gi; + const errorScope = lowerMessage + .replace(nonErrorColonPairs, "") + .replace(nonErrorLogfmtPairs, ""); if ( /(?:^|\s)(?:error|err):?\s/i.test(errorScope) || From 9f1ba37e13879c8f8533048ef739a812bfa9c9ee Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 10:24:29 +0000 Subject: [PATCH 08/11] [autofix.ci] apply automated fixes --- apps/dokploy/__test__/logs/log-classification.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/dokploy/__test__/logs/log-classification.test.ts b/apps/dokploy/__test__/logs/log-classification.test.ts index d97d76a29..edb6b9e51 100644 --- a/apps/dokploy/__test__/logs/log-classification.test.ts +++ b/apps/dokploy/__test__/logs/log-classification.test.ts @@ -29,9 +29,9 @@ test("does not classify explicit non-error key/values as error (#4538)", () => { test("keeps errors whose value merely starts with a no-error word", () => { expect(getLogType("connect failed: no route to host").type).toBe("error"); expect(getLogType("connection failed: no such host").type).toBe("error"); - expect(getLogType("error: none of the configured nodes are available").type).toBe( - "error", - ); + expect( + getLogType("error: none of the configured nodes are available").type, + ).toBe("error"); expect(getLogType("error: nil pointer dereference").type).toBe("error"); expect(getLogType("request failed: 0 bytes received").type).toBe("error"); }); From 94098b2686c809ec8f1bafa8f18aa69a8c268dff Mon Sep 17 00:00:00 2001 From: elisiumm <86828145+elisiumm@users.noreply.github.com> Date: Thu, 6 Aug 2026 14:25:29 +0200 Subject: [PATCH 09/11] fix(ui): expose isStaticSpa toggle for nixpacks + publish directory The backend already honors isStaticSpa for the Nixpacks builder when a publishDirectory is set (getStaticCommand generates an SPA-aware nginx config with try_files fallback to index.html), but the build type form only wired the field for the Static builder. As a result the toggle never rendered for Nixpacks and any save in that mode silently cleared the flag via the discriminated union schema and resetData mapping. Add isStaticSpa to the nixpacks schema variant and resetData case, render the same SPA checkbox once a publish directory is set, and include the field in the save mutation payload for nixpacks. Fixes #4993 --- .../dashboard/application/build/show.tsx | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/apps/dokploy/components/dashboard/application/build/show.tsx b/apps/dokploy/components/dashboard/application/build/show.tsx index 32aee23d3..ac2531f94 100644 --- a/apps/dokploy/components/dashboard/application/build/show.tsx +++ b/apps/dokploy/components/dashboard/application/build/show.tsx @@ -88,6 +88,7 @@ const mySchema = z.discriminatedUnion("buildType", [ z.object({ buildType: z.literal(BuildType.nixpacks), publishDirectory: z.string().optional(), + isStaticSpa: z.boolean().default(false), }), z.object({ buildType: z.literal(BuildType.railpack), @@ -138,6 +139,7 @@ const resetData = (data: ApplicationData): AddTemplate => { return { buildType: BuildType.nixpacks, publishDirectory: data.publishDirectory || undefined, + isStaticSpa: data.isStaticSpa ?? false, }; case BuildType.paketo_buildpacks: return { @@ -179,6 +181,7 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { const buildType = form.watch("buildType"); const railpackVersion = form.watch("railpackVersion"); + const publishDirectory = form.watch("publishDirectory"); const [isManualRailpackVersion, setIsManualRailpackVersion] = useState(false); useEffect(() => { @@ -224,7 +227,10 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { ? data.herokuVersion : null, isStaticSpa: - data.buildType === BuildType.static ? data.isStaticSpa : null, + data.buildType === BuildType.static || + data.buildType === BuildType.nixpacks + ? data.isStaticSpa + : null, railpackVersion: data.buildType === BuildType.railpack ? data.railpackVersion || "0.15.4" @@ -419,6 +425,30 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { )} /> )} + {buildType === BuildType.nixpacks && publishDirectory && ( + ( + + +
+ + + Single Page Application (SPA) + +
+
+ +
+ )} + /> + )} {buildType === BuildType.static && ( Date: Thu, 6 Aug 2026 14:20:29 -0600 Subject: [PATCH 10/11] fix: parse Block I/O and Network I/O stats to numeric MB so monitoring charts render --- .../free/container/docker-block-chart.tsx | 7 ++-- .../free/container/docker-network-chart.tsx | 7 ++-- .../show-free-container-monitoring.tsx | 5 ++- packages/server/src/monitoring/units.ts | 41 +++++++++++++++++++ packages/server/src/monitoring/utils.ts | 9 ++-- 5 files changed, 57 insertions(+), 12 deletions(-) create mode 100644 packages/server/src/monitoring/units.ts diff --git a/apps/dokploy/components/dashboard/monitoring/free/container/docker-block-chart.tsx b/apps/dokploy/components/dashboard/monitoring/free/container/docker-block-chart.tsx index 91db49869..01989753a 100644 --- a/apps/dokploy/components/dashboard/monitoring/free/container/docker-block-chart.tsx +++ b/apps/dokploy/components/dashboard/monitoring/free/container/docker-block-chart.tsx @@ -1,3 +1,4 @@ +import { formatMb, toMb } from "@dokploy/server/monitoring/units"; import { format } from "date-fns"; import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"; import { @@ -29,8 +30,8 @@ export const DockerBlockChart = ({ accumulativeData }: Props) => { const transformedData = accumulativeData.map((item, index) => ({ time: item.time, name: `Point ${index + 1}`, - readMb: item.value.readMb, - writeMb: item.value.writeMb, + readMb: toMb(item.value.readMb), + writeMb: toMb(item.value.writeMb), })); return ( @@ -77,7 +78,7 @@ export const DockerBlockChart = ({ accumulativeData }: Props) => { }} formatter={(value, name) => { const label = name === "readMb" ? "Read" : "Write"; - return [`${value} MB`, label]; + return [formatMb(Number(value)), label]; }} /> } diff --git a/apps/dokploy/components/dashboard/monitoring/free/container/docker-network-chart.tsx b/apps/dokploy/components/dashboard/monitoring/free/container/docker-network-chart.tsx index 56e512d52..92f23e75b 100644 --- a/apps/dokploy/components/dashboard/monitoring/free/container/docker-network-chart.tsx +++ b/apps/dokploy/components/dashboard/monitoring/free/container/docker-network-chart.tsx @@ -1,3 +1,4 @@ +import { formatMb, toMb } from "@dokploy/server/monitoring/units"; import { format } from "date-fns"; import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"; import { @@ -29,8 +30,8 @@ export const DockerNetworkChart = ({ accumulativeData }: Props) => { const transformedData = accumulativeData.map((item, index) => ({ time: item.time, name: `Point ${index + 1}`, - inMB: item.value.inputMb, - outMB: item.value.outputMb, + inMB: toMb(item.value.inputMb), + outMB: toMb(item.value.outputMb), })); return ( @@ -73,7 +74,7 @@ export const DockerNetworkChart = ({ accumulativeData }: Props) => { }} formatter={(value, name) => { const label = name === "inMB" ? "In" : "Out"; - return [`${value} MB`, label]; + return [formatMb(Number(value)), label]; }} /> } diff --git a/apps/dokploy/components/dashboard/monitoring/free/container/show-free-container-monitoring.tsx b/apps/dokploy/components/dashboard/monitoring/free/container/show-free-container-monitoring.tsx index 782b19134..54b7bace4 100644 --- a/apps/dokploy/components/dashboard/monitoring/free/container/show-free-container-monitoring.tsx +++ b/apps/dokploy/components/dashboard/monitoring/free/container/show-free-container-monitoring.tsx @@ -1,3 +1,4 @@ +import { formatMb } from "@dokploy/server/monitoring/units"; import { useEffect, useState } from "react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Progress } from "@/components/ui/progress"; @@ -305,7 +306,7 @@ export const ContainerFreeMonitoring = ({
- {`Read: ${currentData.block.value.readMb} / Write: ${currentData.block.value.writeMb} `} + {`Read: ${formatMb(currentData.block.value.readMb)} / Write: ${formatMb(currentData.block.value.writeMb)}`}
@@ -318,7 +319,7 @@ export const ContainerFreeMonitoring = ({
- {`In MB: ${currentData.network.value.inputMb} / Out MB: ${currentData.network.value.outputMb} `} + {`In: ${formatMb(currentData.network.value.inputMb)} / Out: ${formatMb(currentData.network.value.outputMb)}`}
diff --git a/packages/server/src/monitoring/units.ts b/packages/server/src/monitoring/units.ts new file mode 100644 index 000000000..880e8cdb8 --- /dev/null +++ b/packages/server/src/monitoring/units.ts @@ -0,0 +1,41 @@ +const IO_UNIT_FACTORS: Record = { + B: 1 / 1e6, + kB: 1e-3, + KB: 1e-3, + MB: 1, + GB: 1e3, + TB: 1e6, + KiB: 1024 / 1e6, + MiB: 1024 ** 2 / 1e6, + GiB: 1024 ** 3 / 1e6, + TiB: 1024 ** 4 / 1e6, +}; + +export const toMb = (value: number | string | undefined): number => { + if (typeof value === "number") { + return value; + } + if (!value) { + return 0; + } + const parsed = Number.parseFloat(value); + if (Number.isNaN(parsed)) { + return 0; + } + const unit = value.replace(/[0-9.\s]/g, ""); + return parsed * (IO_UNIT_FACTORS[unit] ?? 1); +}; + +export const parseIoToMb = (raw: string | undefined): number => + Number(toMb(raw).toFixed(6)); + +export const formatMb = (value: number | string | undefined): string => { + const mb = toMb(value); + if (mb >= 1000) { + return `${(mb / 1000).toFixed(2)} GB`; + } + if (mb < 0.01 && mb > 0) { + return `${(mb * 1000).toFixed(2)} kB`; + } + return `${mb.toFixed(2)} MB`; +}; diff --git a/packages/server/src/monitoring/utils.ts b/packages/server/src/monitoring/utils.ts index 2c42b99a6..c2eefc8ae 100644 --- a/packages/server/src/monitoring/utils.ts +++ b/packages/server/src/monitoring/utils.ts @@ -1,6 +1,7 @@ import { promises } from "node:fs"; import { OSUtils } from "node-os-utils"; import { paths } from "../constants"; +import { parseIoToMb } from "./units"; export interface Container { BlockIO: string; @@ -28,13 +29,13 @@ export const recordAdvancedStats = async ( }); await updateStatsFile(appName, "block", { - readMb: stats.BlockIO.split(" ")[0], - writeMb: stats.BlockIO.split(" ")[2], + readMb: parseIoToMb(stats.BlockIO.split(" ")[0]), + writeMb: parseIoToMb(stats.BlockIO.split(" ")[2]), }); await updateStatsFile(appName, "network", { - inputMb: stats.NetIO.split(" ")[0], - outputMb: stats.NetIO.split(" ")[2], + inputMb: parseIoToMb(stats.NetIO.split(" ")[0]), + outputMb: parseIoToMb(stats.NetIO.split(" ")[2]), }); if (appName === "dokploy") { From a8c35f750113581fe8a604bb488fca6bb07bfcfa Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Fri, 7 Aug 2026 01:13:13 -0600 Subject: [PATCH 11/11] fix: pin traefik routing to the isolated network when isolated deployment is enabled --- packages/server/src/utils/docker/domain.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/server/src/utils/docker/domain.ts b/packages/server/src/utils/docker/domain.ts index 26acf2986..2204a71f9 100644 --- a/packages/server/src/utils/docker/domain.ts +++ b/packages/server/src/utils/docker/domain.ts @@ -220,6 +220,17 @@ export const addDomainToCompose = async ( labels.unshift("traefik.swarm.network=dokploy-network"); } } + } else { + const isolatedNetwork = compose.suffix || compose.appName; + if (compose.composeType === "docker-compose") { + if (!labels.includes(`traefik.docker.network=${isolatedNetwork}`)) { + labels.unshift(`traefik.docker.network=${isolatedNetwork}`); + } + } else { + if (!labels.includes(`traefik.swarm.network=${isolatedNetwork}`)) { + labels.unshift(`traefik.swarm.network=${isolatedNetwork}`); + } + } } }