From 7185047eb724362590407bb0fae21c0a3f6821d7 Mon Sep 17 00:00:00 2001 From: lear Date: Wed, 4 Mar 2026 11:07:42 +0300 Subject: [PATCH 01/30] fix: add docker login before rollback and fix execAsyncRemote argument order --- packages/server/src/services/rollbacks.ts | 31 ++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/server/src/services/rollbacks.ts b/packages/server/src/services/rollbacks.ts index 00c60ebc8..301438fed 100644 --- a/packages/server/src/services/rollbacks.ts +++ b/packages/server/src/services/rollbacks.ts @@ -111,7 +111,7 @@ const deleteRollbackImage = async (image: string, serverId?: string | null) => { const command = `docker image rm ${image} --force`; if (serverId) { - await execAsyncRemote(command, serverId); + await execAsyncRemote(serverId, command); } else { await execAsync(command); } @@ -171,6 +171,27 @@ export const rollback = async (rollbackId: string) => { ); }; +const dockerLoginForRegistry = async ( + registry: Registry, + serverId?: string | null, +) => { + const escapedRegistry = shEscape(registry.registryUrl); + const escapedUser = shEscape(registry.username); + const escapedPassword = shEscape(registry.password); + const loginCommand = `printf %s ${escapedPassword} | docker login ${escapedRegistry} -u ${escapedUser} --password-stdin`; + + if (serverId) { + await execAsyncRemote(serverId, loginCommand); + } else { + await execAsync(loginCommand); + } +}; + +function shEscape(s: string | undefined): string { + if (!s) return "''"; + return `'${s.replace(/'/g, `'\\''`)}'`; +} + const rollbackApplication = async ( appName: string, image: string, @@ -188,6 +209,14 @@ const rollbackApplication = async ( throw new Error("Full context is required for rollback"); } + // Ensure Docker daemon is authenticated with the rollback registry + // before updating the swarm service. The authconfig in CreateServiceOptions + // alone is not sufficient — Docker Swarm also relies on the daemon's + // cached credentials (~/.docker/config.json) to distribute auth to nodes. + if (fullContext.rollbackRegistry) { + await dockerLoginForRegistry(fullContext.rollbackRegistry, serverId); + } + const docker = await getRemoteDocker(serverId); // Use the same configuration as mechanizeDockerContainer From d2fabc998dff5c4da58ee5bc5f657a05001f1c2b Mon Sep 17 00:00:00 2001 From: lear Date: Wed, 4 Mar 2026 12:45:57 +0300 Subject: [PATCH 02/30] refactor: reuse safeDockerLoginCommand from registry.ts instead of duplicating shEscape --- packages/server/src/services/registry.ts | 2 +- packages/server/src/services/rollbacks.ts | 16 ++++++---------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/server/src/services/registry.ts b/packages/server/src/services/registry.ts index f9d3977b9..ad18e0b66 100644 --- a/packages/server/src/services/registry.ts +++ b/packages/server/src/services/registry.ts @@ -16,7 +16,7 @@ function shEscape(s: string | undefined): string { return `'${s.replace(/'/g, `'\\''`)}'`; } -function safeDockerLoginCommand( +export function safeDockerLoginCommand( registry: string | undefined, user: string | undefined, pass: string | undefined, diff --git a/packages/server/src/services/rollbacks.ts b/packages/server/src/services/rollbacks.ts index 301438fed..51d978572 100644 --- a/packages/server/src/services/rollbacks.ts +++ b/packages/server/src/services/rollbacks.ts @@ -23,7 +23,7 @@ import { findDeploymentById } from "./deployment"; import type { Mount } from "./mount"; import type { Port } from "./port"; import type { Project } from "./project"; -import type { Registry } from "./registry"; +import { type Registry, safeDockerLoginCommand } from "./registry"; export const createRollback = async ( input: z.infer, @@ -175,10 +175,11 @@ const dockerLoginForRegistry = async ( registry: Registry, serverId?: string | null, ) => { - const escapedRegistry = shEscape(registry.registryUrl); - const escapedUser = shEscape(registry.username); - const escapedPassword = shEscape(registry.password); - const loginCommand = `printf %s ${escapedPassword} | docker login ${escapedRegistry} -u ${escapedUser} --password-stdin`; + const loginCommand = safeDockerLoginCommand( + registry.registryUrl, + registry.username, + registry.password, + ); if (serverId) { await execAsyncRemote(serverId, loginCommand); @@ -187,11 +188,6 @@ const dockerLoginForRegistry = async ( } }; -function shEscape(s: string | undefined): string { - if (!s) return "''"; - return `'${s.replace(/'/g, `'\\''`)}'`; -} - const rollbackApplication = async ( appName: string, image: string, From f1b2cc35b3f1eaa927430918dc6419d8f72b3f1f Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Wed, 4 Mar 2026 21:21:46 -0600 Subject: [PATCH 03/30] fix: update rsync command in web-server backup to exclude special files and devices --- packages/server/src/utils/backups/web-server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/utils/backups/web-server.ts b/packages/server/src/utils/backups/web-server.ts index 4d13ae31a..84f6b863a 100644 --- a/packages/server/src/utils/backups/web-server.ts +++ b/packages/server/src/utils/backups/web-server.ts @@ -67,7 +67,7 @@ export const runWebServerBackup = async (backup: BackupSchedule) => { await execAsync(cleanupCommand); await execAsync( - `rsync -a --ignore-errors ${BASE_PATH}/ ${tempDir}/filesystem/`, + `rsync -a --ignore-errors --no-specials --no-devices ${BASE_PATH}/ ${tempDir}/filesystem/`, ); writeStream.write("Copied filesystem to temp directory\n"); From 48a577e7923f76cfda648ccdd4437f7e681e675d Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Thu, 5 Mar 2026 00:46:13 -0600 Subject: [PATCH 04/30] feat: enhance Docker configuration with environment variables for API version, host, and port --- packages/server/src/constants/index.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/server/src/constants/index.ts b/packages/server/src/constants/index.ts index 745264f24..8ccb2b83e 100644 --- a/packages/server/src/constants/index.ts +++ b/packages/server/src/constants/index.ts @@ -2,8 +2,22 @@ import path from "node:path"; import Docker from "dockerode"; export const IS_CLOUD = process.env.IS_CLOUD === "true"; +export const DOCKER_API_VERSION = process.env.DOCKER_API_VERSION; +export const DOCKER_HOST = process.env.DOCKER_HOST; +export const DOCKER_PORT = process.env.DOCKER_PORT; + export const CLEANUP_CRON_JOB = "50 23 * * *"; -export const docker = new Docker(); +export const docker = new Docker({ + ...(DOCKER_API_VERSION && { + version: DOCKER_API_VERSION, + }), + ...(DOCKER_HOST && { + host: DOCKER_HOST, + }), + ...(DOCKER_PORT && { + port: DOCKER_PORT, + }), +}); // When not set, use the legacy default so 2FA remains working for users who // enabled it before BETTER_AUTH_SECRET was introduced . From d1c4ac20e3ccdf561c1ce38df5c395f1794c05bb Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Thu, 5 Mar 2026 10:48:47 -0600 Subject: [PATCH 05/30] feat: add enableSubmodules and update watchPaths in application schema --- packages/server/src/db/schema/application.ts | 25 ++++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/packages/server/src/db/schema/application.ts b/packages/server/src/db/schema/application.ts index a639d1742..c469ac788 100644 --- a/packages/server/src/db/schema/application.ts +++ b/packages/server/src/db/schema/application.ts @@ -365,12 +365,13 @@ const createSchema = createInsertSchema(applications, { previewPath: z.string().optional(), previewCertificateType: z.enum(["letsencrypt", "none", "custom"]).optional(), previewRequireCollaboratorPermissions: z.boolean().optional(), - watchPaths: z.array(z.string()).optional(), + watchPaths: z.array(z.string()).optional().optional(), previewLabels: z.array(z.string()).optional(), cleanCache: z.boolean().optional(), stopGracePeriodSwarm: z.bigint().nullable(), endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(), ulimitsSwarm: UlimitsSwarmSchema.nullable(), + enableSubmodules: z.boolean().optional(), }); export const apiCreateApplication = createSchema.pick({ @@ -433,13 +434,13 @@ export const apiSaveGithubProvider = createSchema owner: true, buildPath: true, githubId: true, - watchPaths: true, - enableSubmodules: true, }) .required() .extend({ triggerType: z.enum(["push", "tag"]).default("push"), - }); + }) + .required() + .merge(createSchema.pick({ enableSubmodules: true, watchPaths: true })); export const apiSaveGitlabProvider = createSchema .pick({ @@ -451,10 +452,9 @@ export const apiSaveGitlabProvider = createSchema gitlabId: true, gitlabProjectId: true, gitlabPathNamespace: true, - watchPaths: true, - enableSubmodules: true, }) - .required(); + .required() + .merge(createSchema.pick({ enableSubmodules: true, watchPaths: true })); export const apiSaveBitbucketProvider = createSchema .pick({ @@ -465,10 +465,9 @@ export const apiSaveBitbucketProvider = createSchema bitbucketRepositorySlug: true, bitbucketId: true, applicationId: true, - watchPaths: true, - enableSubmodules: true, }) - .required(); + .required() + .merge(createSchema.pick({ enableSubmodules: true, watchPaths: true })); export const apiSaveGiteaProvider = createSchema .pick({ @@ -478,10 +477,9 @@ export const apiSaveGiteaProvider = createSchema giteaOwner: true, giteaRepository: true, giteaId: true, - watchPaths: true, - enableSubmodules: true, }) - .required(); + .required() + .merge(createSchema.pick({ enableSubmodules: true, watchPaths: true })); export const apiSaveDockerProvider = createSchema .pick({ @@ -506,6 +504,7 @@ export const apiSaveGitProvider = createSchema .merge( createSchema.pick({ customGitSSHKeyId: true, + enableSubmodules: true, }), ); From 34304526b191a720679ff9c9152a8eed39ed0381 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Thu, 5 Mar 2026 11:08:31 -0600 Subject: [PATCH 06/30] fix: skip redirect middleware for preview deployments to prevent wildcard subdomain inheritance --- packages/server/src/utils/traefik/domain.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/server/src/utils/traefik/domain.ts b/packages/server/src/utils/traefik/domain.ts index 97400b1b9..6a328a1d9 100644 --- a/packages/server/src/utils/traefik/domain.ts +++ b/packages/server/src/utils/traefik/domain.ts @@ -152,16 +152,13 @@ export const createRouterConfig = async ( } if ((entryPoint === "websecure" && https) || !https) { - // redirects - for (const redirect of redirects) { - let middlewareName = `redirect-${appName}-${redirect.uniqueConfigKey}`; - if (domain.domainType === "preview") { - middlewareName = `redirect-${appName.replace( - /^preview-(.+)-[^-]+$/, - "$1", - )}-${redirect.uniqueConfigKey}`; + // redirects - skip for preview deployments as wildcard subdomains + // should not inherit parent redirect rules (e.g., www-redirect) + if (domain.domainType !== "preview") { + for (const redirect of redirects) { + const middlewareName = `redirect-${appName}-${redirect.uniqueConfigKey}`; + routerConfig.middlewares?.push(middlewareName); } - routerConfig.middlewares?.push(middlewareName); } // security From 9ae2ebff4649d2112e00a5bd64c31e7471299661 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Fri, 6 Mar 2026 08:27:12 -0600 Subject: [PATCH 07/30] Bump version from v0.28.3 to v0.28.4 --- 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 a41f08fd6..6e9802910 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -1,6 +1,6 @@ { "name": "dokploy", - "version": "v0.28.3", + "version": "v0.28.4", "private": true, "license": "Apache-2.0", "type": "module", From 70c261d02181de5c063dd78b37299f3b05f2ce60 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Fri, 6 Mar 2026 11:43:57 -0600 Subject: [PATCH 08/30] Update packages/server/src/constants/index.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- packages/server/src/constants/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/server/src/constants/index.ts b/packages/server/src/constants/index.ts index 8ccb2b83e..3d28c4e15 100644 --- a/packages/server/src/constants/index.ts +++ b/packages/server/src/constants/index.ts @@ -4,7 +4,9 @@ import Docker from "dockerode"; export const IS_CLOUD = process.env.IS_CLOUD === "true"; export const DOCKER_API_VERSION = process.env.DOCKER_API_VERSION; export const DOCKER_HOST = process.env.DOCKER_HOST; -export const DOCKER_PORT = process.env.DOCKER_PORT; +export const DOCKER_PORT = process.env.DOCKER_PORT + ? Number(process.env.DOCKER_PORT) + : undefined; export const CLEANUP_CRON_JOB = "50 23 * * *"; export const docker = new Docker({ From 2eae756cec1970f19cd1e814be8a060a249b3fb6 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Fri, 6 Mar 2026 23:18:29 -0600 Subject: [PATCH 09/30] Update dependencies in pnpm-lock.yaml and package.json for @codemirror packages - Added @codemirror/search version 6.6.0. - Updated @codemirror/view to version 6.39.15 across multiple files. - Adjusted imports in code-editor.tsx to include search functionality. This update ensures compatibility with the latest features and improvements in the CodeMirror library. --- .../dokploy/components/shared/code-editor.tsx | 5 +- apps/dokploy/package.json | 3 +- pnpm-lock.yaml | 48 ++++++++----------- 3 files changed, 27 insertions(+), 29 deletions(-) diff --git a/apps/dokploy/components/shared/code-editor.tsx b/apps/dokploy/components/shared/code-editor.tsx index 8fe583339..29f18a3e0 100644 --- a/apps/dokploy/components/shared/code-editor.tsx +++ b/apps/dokploy/components/shared/code-editor.tsx @@ -9,7 +9,8 @@ import { yaml } from "@codemirror/lang-yaml"; import { StreamLanguage } from "@codemirror/language"; import { properties } from "@codemirror/legacy-modes/mode/properties"; import { shell } from "@codemirror/legacy-modes/mode/shell"; -import { EditorView } from "@codemirror/view"; +import { search, searchKeymap } from "@codemirror/search"; +import { EditorView, keymap } from "@codemirror/view"; import { githubDark, githubLight } from "@uiw/codemirror-theme-github"; import CodeMirror, { type ReactCodeMirrorProps } from "@uiw/react-codemirror"; import { useTheme } from "next-themes"; @@ -155,6 +156,8 @@ export const CodeEditor = ({ }} theme={resolvedTheme === "dark" ? githubDark : githubLight} extensions={[ + search(), + keymap.of(searchKeymap), language === "yaml" ? yaml() : language === "json" diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index 6e9802910..7ed8a2e6a 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -53,7 +53,8 @@ "@codemirror/lang-yaml": "^6.1.2", "@codemirror/language": "^6.11.0", "@codemirror/legacy-modes": "6.4.0", - "@codemirror/view": "6.29.0", + "@codemirror/search": "^6.6.0", + "@codemirror/view": "^6.39.15", "@dokploy/server": "workspace:*", "@dokploy/trpc-openapi": "0.0.17", "@faker-js/faker": "^8.4.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index efd3780e7..3ccae53df 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -131,9 +131,12 @@ importers: '@codemirror/legacy-modes': specifier: 6.4.0 version: 6.4.0 + '@codemirror/search': + specifier: ^6.6.0 + version: 6.6.0 '@codemirror/view': - specifier: 6.29.0 - version: 6.29.0 + specifier: ^6.39.15 + version: 6.39.15 '@dokploy/server': specifier: workspace:* version: link:../../packages/server @@ -241,10 +244,10 @@ importers: version: 11.10.0(typescript@5.9.3) '@uiw/codemirror-theme-github': specifier: ^4.23.12 - version: 4.25.4(@codemirror/language@6.12.1)(@codemirror/state@6.5.4)(@codemirror/view@6.29.0) + version: 4.25.4(@codemirror/language@6.12.1)(@codemirror/state@6.5.4)(@codemirror/view@6.39.15) '@uiw/react-codemirror': specifier: ^4.23.12 - version: 4.25.4(@babel/runtime@7.28.6)(@codemirror/autocomplete@6.20.0)(@codemirror/language@6.12.1)(@codemirror/lint@6.9.4)(@codemirror/search@6.6.0)(@codemirror/state@6.5.4)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.29.0)(codemirror@6.0.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 4.25.4(@babel/runtime@7.28.6)(@codemirror/autocomplete@6.20.0)(@codemirror/language@6.12.1)(@codemirror/lint@6.9.4)(@codemirror/search@6.6.0)(@codemirror/state@6.5.4)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.39.15)(codemirror@6.0.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@xterm/addon-attach': specifier: 0.10.0 version: 0.10.0(@xterm/xterm@5.5.0) @@ -1285,9 +1288,6 @@ packages: '@codemirror/theme-one-dark@6.1.3': resolution: {integrity: sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==} - '@codemirror/view@6.29.0': - resolution: {integrity: sha512-ED4ims4fkf7eOA+HYLVP8VVg3NMllt1FPm9PEJBfYFnidKlRITBaua38u68L1F60eNtw2YNcDN5jsIzhKZwWQA==} - '@codemirror/view@6.39.15': resolution: {integrity: sha512-aCWjgweIIXLBHh7bY6cACvXuyrZ0xGafjQ2VInjp4RM4gMfscK5uESiNdrH0pE+e1lZr2B4ONGsjchl2KsKZzg==} @@ -8793,14 +8793,14 @@ snapshots: dependencies: '@codemirror/language': 6.12.1 '@codemirror/state': 6.5.4 - '@codemirror/view': 6.29.0 + '@codemirror/view': 6.39.15 '@lezer/common': 1.5.1 '@codemirror/commands@6.10.2': dependencies: '@codemirror/language': 6.12.1 '@codemirror/state': 6.5.4 - '@codemirror/view': 6.29.0 + '@codemirror/view': 6.39.15 '@lezer/common': 1.5.1 '@codemirror/lang-json@6.0.2': @@ -8821,7 +8821,7 @@ snapshots: '@codemirror/language@6.12.1': dependencies: '@codemirror/state': 6.5.4 - '@codemirror/view': 6.29.0 + '@codemirror/view': 6.39.15 '@lezer/common': 1.5.1 '@lezer/highlight': 1.2.3 '@lezer/lr': 1.4.8 @@ -8851,15 +8851,9 @@ snapshots: dependencies: '@codemirror/language': 6.12.1 '@codemirror/state': 6.5.4 - '@codemirror/view': 6.29.0 + '@codemirror/view': 6.39.15 '@lezer/highlight': 1.2.3 - '@codemirror/view@6.29.0': - dependencies: - '@codemirror/state': 6.5.4 - style-mod: 4.1.3 - w3c-keyname: 2.2.8 - '@codemirror/view@6.39.15': dependencies: '@codemirror/state': 6.5.4 @@ -12094,7 +12088,7 @@ snapshots: dependencies: '@types/node': 24.10.13 - '@uiw/codemirror-extensions-basic-setup@4.25.4(@codemirror/autocomplete@6.20.0)(@codemirror/commands@6.10.2)(@codemirror/language@6.12.1)(@codemirror/lint@6.9.4)(@codemirror/search@6.6.0)(@codemirror/state@6.5.4)(@codemirror/view@6.29.0)': + '@uiw/codemirror-extensions-basic-setup@4.25.4(@codemirror/autocomplete@6.20.0)(@codemirror/commands@6.10.2)(@codemirror/language@6.12.1)(@codemirror/lint@6.9.4)(@codemirror/search@6.6.0)(@codemirror/state@6.5.4)(@codemirror/view@6.39.15)': dependencies: '@codemirror/autocomplete': 6.20.0 '@codemirror/commands': 6.10.2 @@ -12102,30 +12096,30 @@ snapshots: '@codemirror/lint': 6.9.4 '@codemirror/search': 6.6.0 '@codemirror/state': 6.5.4 - '@codemirror/view': 6.29.0 + '@codemirror/view': 6.39.15 - '@uiw/codemirror-theme-github@4.25.4(@codemirror/language@6.12.1)(@codemirror/state@6.5.4)(@codemirror/view@6.29.0)': + '@uiw/codemirror-theme-github@4.25.4(@codemirror/language@6.12.1)(@codemirror/state@6.5.4)(@codemirror/view@6.39.15)': dependencies: - '@uiw/codemirror-themes': 4.25.4(@codemirror/language@6.12.1)(@codemirror/state@6.5.4)(@codemirror/view@6.29.0) + '@uiw/codemirror-themes': 4.25.4(@codemirror/language@6.12.1)(@codemirror/state@6.5.4)(@codemirror/view@6.39.15) transitivePeerDependencies: - '@codemirror/language' - '@codemirror/state' - '@codemirror/view' - '@uiw/codemirror-themes@4.25.4(@codemirror/language@6.12.1)(@codemirror/state@6.5.4)(@codemirror/view@6.29.0)': + '@uiw/codemirror-themes@4.25.4(@codemirror/language@6.12.1)(@codemirror/state@6.5.4)(@codemirror/view@6.39.15)': dependencies: '@codemirror/language': 6.12.1 '@codemirror/state': 6.5.4 - '@codemirror/view': 6.29.0 + '@codemirror/view': 6.39.15 - '@uiw/react-codemirror@4.25.4(@babel/runtime@7.28.6)(@codemirror/autocomplete@6.20.0)(@codemirror/language@6.12.1)(@codemirror/lint@6.9.4)(@codemirror/search@6.6.0)(@codemirror/state@6.5.4)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.29.0)(codemirror@6.0.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + '@uiw/react-codemirror@4.25.4(@babel/runtime@7.28.6)(@codemirror/autocomplete@6.20.0)(@codemirror/language@6.12.1)(@codemirror/lint@6.9.4)(@codemirror/search@6.6.0)(@codemirror/state@6.5.4)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.39.15)(codemirror@6.0.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.28.6 '@codemirror/commands': 6.10.2 '@codemirror/state': 6.5.4 '@codemirror/theme-one-dark': 6.1.3 - '@codemirror/view': 6.29.0 - '@uiw/codemirror-extensions-basic-setup': 4.25.4(@codemirror/autocomplete@6.20.0)(@codemirror/commands@6.10.2)(@codemirror/language@6.12.1)(@codemirror/lint@6.9.4)(@codemirror/search@6.6.0)(@codemirror/state@6.5.4)(@codemirror/view@6.29.0) + '@codemirror/view': 6.39.15 + '@uiw/codemirror-extensions-basic-setup': 4.25.4(@codemirror/autocomplete@6.20.0)(@codemirror/commands@6.10.2)(@codemirror/language@6.12.1)(@codemirror/lint@6.9.4)(@codemirror/search@6.6.0)(@codemirror/state@6.5.4)(@codemirror/view@6.39.15) codemirror: 6.0.2 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -12772,7 +12766,7 @@ snapshots: '@codemirror/lint': 6.9.4 '@codemirror/search': 6.6.0 '@codemirror/state': 6.5.4 - '@codemirror/view': 6.29.0 + '@codemirror/view': 6.39.15 color-convert@2.0.1: dependencies: From 38b20450dc84ae603a45dce05faf9771604df264 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sat, 7 Mar 2026 00:21:29 -0600 Subject: [PATCH 10/30] fix: update Docker network creation command to specify driver for stack deployments --- packages/server/src/utils/builders/compose.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/utils/builders/compose.ts b/packages/server/src/utils/builders/compose.ts index 5eede59d5..ca2b8a6b5 100644 --- a/packages/server/src/utils/builders/compose.ts +++ b/packages/server/src/utils/builders/compose.ts @@ -53,7 +53,7 @@ Compose Type: ${composeType} ✅`; cd "${projectPath}"; - ${compose.isolatedDeployment ? `docker network inspect ${compose.appName} >/dev/null 2>&1 || docker network create --attachable ${compose.appName}` : ""} + ${compose.isolatedDeployment ? `docker network inspect ${compose.appName} >/dev/null 2>&1 || docker network create ${compose.composeType === "stack" ? "--driver overlay" : ""} --attachable ${compose.appName}` : ""} env -i PATH="$PATH" ${exportEnvCommand} docker ${command.split(" ").join(" ")} 2>&1 || { echo "Error: ❌ Docker command failed"; exit 1; } ${compose.isolatedDeployment ? `docker network connect ${compose.appName} $(docker ps --filter "name=dokploy-traefik" -q) >/dev/null 2>&1` : ""} From cedec5239fcf60df8808a9e1944803eb36563b71 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sat, 7 Mar 2026 00:33:54 -0600 Subject: [PATCH 11/30] refactor: update Gitea and GitLab URL handling to prioritize internal URLs if available --- packages/server/src/utils/providers/gitea.ts | 6 +++--- packages/server/src/utils/providers/gitlab.ts | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/server/src/utils/providers/gitea.ts b/packages/server/src/utils/providers/gitea.ts index 4d26a9212..a2e061fcd 100644 --- a/packages/server/src/utils/providers/gitea.ts +++ b/packages/server/src/utils/providers/gitea.ts @@ -211,7 +211,7 @@ export const testGiteaConnection = async (input: { giteaId: string }) => { }); } - const baseUrl = provider.giteaUrl.replace(/\/+$/, ""); + const baseUrl = (provider.giteaInternalUrl || provider.giteaUrl).replace(/\/+$/, ""); // Use /user/repos to get authenticated user's repositories with pagination let allRepos = 0; @@ -268,7 +268,7 @@ export const getGiteaRepositories = async (giteaId?: string) => { await refreshGiteaToken(giteaId); const giteaProvider = await findGiteaById(giteaId); - const baseUrl = giteaProvider.giteaUrl.replace(/\/+$/, ""); + const baseUrl = (giteaProvider.giteaInternalUrl || giteaProvider.giteaUrl).replace(/\/+$/, ""); // Use /user/repos to get authenticated user's repositories with pagination let allRepositories: any[] = []; @@ -333,7 +333,7 @@ export const getGiteaBranches = async (input: { const giteaProvider = await findGiteaById(input.giteaId); - const baseUrl = giteaProvider.giteaUrl.replace(/\/+$/, ""); + const baseUrl = (giteaProvider.giteaInternalUrl || giteaProvider.giteaUrl).replace(/\/+$/, ""); // Handle pagination for branches let allBranches: any[] = []; diff --git a/packages/server/src/utils/providers/gitlab.ts b/packages/server/src/utils/providers/gitlab.ts index 41126cd29..0629895aa 100644 --- a/packages/server/src/utils/providers/gitlab.ts +++ b/packages/server/src/utils/providers/gitlab.ts @@ -214,10 +214,11 @@ export const getGitlabBranches = async (input: { const allBranches = []; let page = 1; const perPage = 100; // GitLab's max per page is 100 + const baseUrl = (gitlabProvider.gitlabInternalUrl || gitlabProvider.gitlabUrl).replace(/\/+$/, ""); while (true) { const branchesResponse = await fetch( - `${gitlabProvider.gitlabUrl}/api/v4/projects/${input.id}/repository/branches?page=${page}&per_page=${perPage}`, + `${baseUrl}/api/v4/projects/${input.id}/repository/branches?page=${page}&per_page=${perPage}`, { headers: { Authorization: `Bearer ${gitlabProvider.accessToken}`, @@ -292,10 +293,11 @@ export const validateGitlabProvider = async (gitlabProvider: Gitlab) => { const allProjects = []; let page = 1; const perPage = 100; // GitLab's max per page is 100 + const baseUrl = (gitlabProvider.gitlabInternalUrl || gitlabProvider.gitlabUrl).replace(/\/+$/, ""); while (true) { const response = await fetch( - `${gitlabProvider.gitlabUrl}/api/v4/projects?membership=true&page=${page}&per_page=${perPage}`, + `${baseUrl}/api/v4/projects?membership=true&page=${page}&per_page=${perPage}`, { headers: { Authorization: `Bearer ${gitlabProvider.accessToken}`, From 28cc361c473bfada32505207c9917b3d52223b40 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 7 Mar 2026 06:34:27 +0000 Subject: [PATCH 12/30] [autofix.ci] apply automated fixes --- packages/server/src/utils/providers/gitea.ts | 13 ++++++++++--- packages/server/src/utils/providers/gitlab.ts | 8 ++++++-- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/server/src/utils/providers/gitea.ts b/packages/server/src/utils/providers/gitea.ts index a2e061fcd..46dc5381b 100644 --- a/packages/server/src/utils/providers/gitea.ts +++ b/packages/server/src/utils/providers/gitea.ts @@ -211,7 +211,10 @@ export const testGiteaConnection = async (input: { giteaId: string }) => { }); } - const baseUrl = (provider.giteaInternalUrl || provider.giteaUrl).replace(/\/+$/, ""); + const baseUrl = (provider.giteaInternalUrl || provider.giteaUrl).replace( + /\/+$/, + "", + ); // Use /user/repos to get authenticated user's repositories with pagination let allRepos = 0; @@ -268,7 +271,9 @@ export const getGiteaRepositories = async (giteaId?: string) => { await refreshGiteaToken(giteaId); const giteaProvider = await findGiteaById(giteaId); - const baseUrl = (giteaProvider.giteaInternalUrl || giteaProvider.giteaUrl).replace(/\/+$/, ""); + const baseUrl = ( + giteaProvider.giteaInternalUrl || giteaProvider.giteaUrl + ).replace(/\/+$/, ""); // Use /user/repos to get authenticated user's repositories with pagination let allRepositories: any[] = []; @@ -333,7 +338,9 @@ export const getGiteaBranches = async (input: { const giteaProvider = await findGiteaById(input.giteaId); - const baseUrl = (giteaProvider.giteaInternalUrl || giteaProvider.giteaUrl).replace(/\/+$/, ""); + const baseUrl = ( + giteaProvider.giteaInternalUrl || giteaProvider.giteaUrl + ).replace(/\/+$/, ""); // Handle pagination for branches let allBranches: any[] = []; diff --git a/packages/server/src/utils/providers/gitlab.ts b/packages/server/src/utils/providers/gitlab.ts index 0629895aa..e8e4af2a5 100644 --- a/packages/server/src/utils/providers/gitlab.ts +++ b/packages/server/src/utils/providers/gitlab.ts @@ -214,7 +214,9 @@ export const getGitlabBranches = async (input: { const allBranches = []; let page = 1; const perPage = 100; // GitLab's max per page is 100 - const baseUrl = (gitlabProvider.gitlabInternalUrl || gitlabProvider.gitlabUrl).replace(/\/+$/, ""); + const baseUrl = ( + gitlabProvider.gitlabInternalUrl || gitlabProvider.gitlabUrl + ).replace(/\/+$/, ""); while (true) { const branchesResponse = await fetch( @@ -293,7 +295,9 @@ export const validateGitlabProvider = async (gitlabProvider: Gitlab) => { const allProjects = []; let page = 1; const perPage = 100; // GitLab's max per page is 100 - const baseUrl = (gitlabProvider.gitlabInternalUrl || gitlabProvider.gitlabUrl).replace(/\/+$/, ""); + const baseUrl = ( + gitlabProvider.gitlabInternalUrl || gitlabProvider.gitlabUrl + ).replace(/\/+$/, ""); while (true) { const response = await fetch( From 4be25da185672c38f267b48929b74d59d9b08d0e Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sat, 7 Mar 2026 00:41:14 -0600 Subject: [PATCH 13/30] fix: add error handling for volume backup notification sending --- .../server/src/utils/volume-backups/utils.ts | 42 +++++++++++-------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/packages/server/src/utils/volume-backups/utils.ts b/packages/server/src/utils/volume-backups/utils.ts index b508c6b88..20cfbb527 100644 --- a/packages/server/src/utils/volume-backups/utils.ts +++ b/packages/server/src/utils/volume-backups/utils.ts @@ -131,14 +131,18 @@ export const runVolumeBackup = async (volumeBackupId: string) => { ? "mongodb" : volumeBackup.serviceType; - await sendVolumeBackupNotifications({ - projectName, - applicationName: volumeBackup.name, - volumeName: volumeBackup.volumeName, - serviceType: mappedServiceType, - type: "success", - organizationId, - }); + try { + await sendVolumeBackupNotifications({ + projectName, + applicationName: volumeBackup.name, + volumeName: volumeBackup.volumeName, + serviceType: mappedServiceType, + type: "success", + organizationId, + }); + } catch (notificationError) { + console.error("Failed to send volume backup success notification", notificationError); + } } catch (error) { const { VOLUME_BACKUPS_PATH } = paths(!!serverId); const volumeBackupPath = path.join( @@ -160,14 +164,18 @@ export const runVolumeBackup = async (volumeBackupId: string) => { ? "mongodb" : volumeBackup.serviceType; - await sendVolumeBackupNotifications({ - projectName, - applicationName: volumeBackup.name, - volumeName: volumeBackup.volumeName, - serviceType: mappedServiceType, - type: "error", - organizationId, - errorMessage: error instanceof Error ? error.message : String(error), - }); + try { + await sendVolumeBackupNotifications({ + projectName, + applicationName: volumeBackup.name, + volumeName: volumeBackup.volumeName, + serviceType: mappedServiceType, + type: "error", + organizationId, + errorMessage: error instanceof Error ? error.message : String(error), + }); + } catch (notificationError) { + console.error("Failed to send volume backup error notification", notificationError); + } } }; From f961dc6e7a6fa03565d053b7fceeda186990268c Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 7 Mar 2026 06:41:44 +0000 Subject: [PATCH 14/30] [autofix.ci] apply automated fixes --- packages/server/src/utils/volume-backups/utils.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/server/src/utils/volume-backups/utils.ts b/packages/server/src/utils/volume-backups/utils.ts index 20cfbb527..d9e0b3bd5 100644 --- a/packages/server/src/utils/volume-backups/utils.ts +++ b/packages/server/src/utils/volume-backups/utils.ts @@ -141,7 +141,10 @@ export const runVolumeBackup = async (volumeBackupId: string) => { organizationId, }); } catch (notificationError) { - console.error("Failed to send volume backup success notification", notificationError); + console.error( + "Failed to send volume backup success notification", + notificationError, + ); } } catch (error) { const { VOLUME_BACKUPS_PATH } = paths(!!serverId); @@ -175,7 +178,10 @@ export const runVolumeBackup = async (volumeBackupId: string) => { errorMessage: error instanceof Error ? error.message : String(error), }); } catch (notificationError) { - console.error("Failed to send volume backup error notification", notificationError); + console.error( + "Failed to send volume backup error notification", + notificationError, + ); } } }; From b87f8cc5d828d7c37c1870bea7201a57f0775c7f Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sat, 7 Mar 2026 00:51:28 -0600 Subject: [PATCH 15/30] refactor: streamline deployment cleanup by consolidating removeLastTenDeployments calls --- packages/server/src/services/deployment.ts | 137 +++++++++++---------- 1 file changed, 75 insertions(+), 62 deletions(-) diff --git a/packages/server/src/services/deployment.ts b/packages/server/src/services/deployment.ts index 891dc553f..a9f059465 100644 --- a/packages/server/src/services/deployment.ts +++ b/packages/server/src/services/deployment.ts @@ -117,12 +117,12 @@ export const createDeployment = async ( >, ) => { const application = await findApplicationById(deployment.applicationId); + await removeLastTenDeployments( + deployment.applicationId, + "application", + application.serverId, + ); try { - await removeLastTenDeployments( - deployment.applicationId, - "application", - application.serverId, - ); const serverId = application.buildServerId || application.serverId; const { LOGS_PATH } = paths(!!serverId); @@ -200,13 +200,12 @@ export const createDeploymentPreview = async ( const previewDeployment = await findPreviewDeploymentById( deployment.previewDeploymentId, ); + await removeLastTenDeployments( + deployment.previewDeploymentId, + "previewDeployment", + previewDeployment?.application?.serverId, + ); try { - await removeLastTenDeployments( - deployment.previewDeploymentId, - "previewDeployment", - previewDeployment?.application?.serverId, - ); - const appName = `${previewDeployment.appName}`; const { LOGS_PATH } = paths(!!previewDeployment?.application?.serverId); const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss"); @@ -281,12 +280,12 @@ export const createDeploymentCompose = async ( >, ) => { const compose = await findComposeById(deployment.composeId); + await removeLastTenDeployments( + deployment.composeId, + "compose", + compose.serverId, + ); try { - await removeLastTenDeployments( - deployment.composeId, - "compose", - compose.serverId, - ); const { LOGS_PATH } = paths(!!compose.serverId); const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss"); const fileName = `${compose.appName}-${formattedDateTime}.log`; @@ -369,8 +368,8 @@ export const createDeploymentBackup = async ( } else if (backup.backupType === "compose") { serverId = backup.compose?.serverId; } + await removeLastTenDeployments(deployment.backupId, "backup", serverId); try { - await removeLastTenDeployments(deployment.backupId, "backup", serverId); const { LOGS_PATH } = paths(!!serverId); const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss"); const fileName = `${backup.appName}-${formattedDateTime}.log`; @@ -439,12 +438,12 @@ export const createDeploymentSchedule = async ( ) => { const schedule = await findScheduleById(deployment.scheduleId); + const serverId = + schedule.application?.serverId || + schedule.compose?.serverId || + schedule.server?.serverId; + await removeLastTenDeployments(deployment.scheduleId, "schedule", serverId); try { - const serverId = - schedule.application?.serverId || - schedule.compose?.serverId || - schedule.server?.serverId; - await removeLastTenDeployments(deployment.scheduleId, "schedule", serverId); const { SCHEDULES_PATH } = paths(!!serverId); const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss"); const fileName = `${schedule.appName}-${formattedDateTime}.log`; @@ -515,14 +514,14 @@ export const createDeploymentVolumeBackup = async ( ) => { const volumeBackup = await findVolumeBackupById(deployment.volumeBackupId); + const serverId = + volumeBackup.application?.serverId || volumeBackup.compose?.serverId; + await removeLastTenDeployments( + deployment.volumeBackupId, + "volumeBackup", + serverId, + ); try { - const serverId = - volumeBackup.application?.serverId || volumeBackup.compose?.serverId; - await removeLastTenDeployments( - deployment.volumeBackupId, - "volumeBackup", - serverId, - ); const { VOLUME_BACKUPS_PATH } = paths(!!serverId); const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss"); const fileName = `${volumeBackup.appName}-${formattedDateTime}.log`; @@ -601,24 +600,23 @@ export const removeDeployment = async (deploymentId: string) => { .then((result) => result[0]); if (!deployment) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Deployment not found", - }); + return null; } - const command = ` - rm -f ${deployment.logPath}; - `; - if (deployment.serverId) { - await execAsyncRemote(deployment.serverId, command); - } else { - await execAsync(command); + + const logPath = path.join(deployment.logPath); + if (logPath && logPath !== ".") { + const command = `rm -f ${logPath};`; + if (deployment.serverId) { + await execAsyncRemote(deployment.serverId, command); + } else { + await execAsync(command); + } } return deployment; } catch (error) { const message = - error instanceof Error ? error.message : "Error creating the deployment"; + error instanceof Error ? error.message : "Error removing the deployment"; throw new TRPCError({ code: "BAD_REQUEST", message, @@ -686,34 +684,49 @@ const removeLastTenDeployments = async ( if (serverId) { let command = ""; for (const oldDeployment of deploymentsToDelete) { - const logPath = path.join(oldDeployment.logPath); - if (oldDeployment.rollbackId) { - await removeRollbackById(oldDeployment.rollbackId); - } + try { + const logPath = path.join(oldDeployment.logPath); + if (oldDeployment.rollbackId) { + await removeRollbackById(oldDeployment.rollbackId); + } - if (logPath !== ".") { - command += ` - rm -rf ${logPath}; - `; + if (logPath && logPath !== ".") { + command += `rm -rf ${logPath};`; + } + await removeDeployment(oldDeployment.deploymentId); + } catch (err) { + console.error( + `Failed to remove deployment ${oldDeployment.deploymentId} during cleanup:`, + err, + ); } - await removeDeployment(oldDeployment.deploymentId); } - await execAsyncRemote(serverId, command); + if (command) { + await execAsyncRemote(serverId, command); + } } else { for (const oldDeployment of deploymentsToDelete) { - if (oldDeployment.rollbackId) { - await removeRollbackById(oldDeployment.rollbackId); + try { + if (oldDeployment.rollbackId) { + await removeRollbackById(oldDeployment.rollbackId); + } + const logPath = path.join(oldDeployment.logPath); + if ( + logPath && + logPath !== "." && + existsSync(logPath) && + !oldDeployment.errorMessage + ) { + await fsPromises.unlink(logPath); + } + await removeDeployment(oldDeployment.deploymentId); + } catch (err) { + console.error( + `Failed to remove deployment ${oldDeployment.deploymentId} during cleanup:`, + err, + ); } - const logPath = path.join(oldDeployment.logPath); - if ( - existsSync(logPath) && - !oldDeployment.errorMessage && - logPath !== "." - ) { - await fsPromises.unlink(logPath); - } - await removeDeployment(oldDeployment.deploymentId); } } } From 808001d8dea7e402a783b2e2882316d00d4cae0a Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sat, 7 Mar 2026 01:22:53 -0600 Subject: [PATCH 16/30] refactor: enhance volume backup path handling to ensure proper prefix usage --- packages/server/src/utils/volume-backups/backup.ts | 5 ++++- packages/server/src/utils/volume-backups/utils.ts | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/server/src/utils/volume-backups/backup.ts b/packages/server/src/utils/volume-backups/backup.ts index 3d229ef64..ad3eea29f 100644 --- a/packages/server/src/utils/volume-backups/backup.ts +++ b/packages/server/src/utils/volume-backups/backup.ts @@ -13,7 +13,10 @@ export const backupVolume = async ( const { VOLUME_BACKUPS_PATH, VOLUME_BACKUP_LOCK_PATH } = paths(!!serverId); const destination = volumeBackup.destination; const backupFileName = `${volumeName}-${new Date().toISOString()}.tar`; - const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`; + const effectivePrefix = prefix + ? normalizeS3Path(prefix) + : `${volumeBackup.appName}/`; + const bucketDestination = `${effectivePrefix}${backupFileName}`; const rcloneFlags = getS3Credentials(volumeBackup.destination); const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`; const volumeBackupPath = path.join(VOLUME_BACKUPS_PATH, volumeBackup.appName); diff --git a/packages/server/src/utils/volume-backups/utils.ts b/packages/server/src/utils/volume-backups/utils.ts index d9e0b3bd5..d179dd096 100644 --- a/packages/server/src/utils/volume-backups/utils.ts +++ b/packages/server/src/utils/volume-backups/utils.ts @@ -81,7 +81,9 @@ const cleanupOldVolumeBackups = async ( try { const rcloneFlags = getS3Credentials(destination); - const normalizedPrefix = normalizeS3Path(prefix); + const normalizedPrefix = prefix + ? normalizeS3Path(prefix) + : `${volumeBackup.appName}/`; const backupFilesPath = `:s3:${destination.bucket}/${normalizedPrefix}`; const listCommand = `rclone lsf ${rcloneFlags.join(" ")} --include \"${volumeName}-*.tar\" :s3:${destination.bucket}/${normalizedPrefix}`; const sortAndPick = `sort -r | tail -n +$((${keepLatestCount}+1)) | xargs -I{}`; From a5a4a1a818dfbc039eeac0bc55bd2b0b2d79e2e9 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sat, 7 Mar 2026 01:48:11 -0600 Subject: [PATCH 17/30] refactor: update backup file paths to include app name for better organization --- packages/server/src/utils/backups/compose.ts | 2 +- packages/server/src/utils/backups/index.ts | 12 ++++-------- packages/server/src/utils/backups/mariadb.ts | 2 +- packages/server/src/utils/backups/mongo.ts | 2 +- packages/server/src/utils/backups/mysql.ts | 2 +- packages/server/src/utils/backups/postgres.ts | 2 +- packages/server/src/utils/backups/web-server.ts | 2 +- packages/server/src/utils/volume-backups/backup.ts | 5 +---- packages/server/src/utils/volume-backups/utils.ts | 7 ++----- 9 files changed, 13 insertions(+), 23 deletions(-) diff --git a/packages/server/src/utils/backups/compose.ts b/packages/server/src/utils/backups/compose.ts index 1963f2c91..28124f809 100644 --- a/packages/server/src/utils/backups/compose.ts +++ b/packages/server/src/utils/backups/compose.ts @@ -20,7 +20,7 @@ export const runComposeBackup = async ( const { prefix, databaseType } = backup; const destination = backup.destination; const backupFileName = `${new Date().toISOString()}.sql.gz`; - const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`; + const bucketDestination = `${backup.appName}/${normalizeS3Path(prefix)}${backupFileName}`; const deployment = await createDeploymentBackup({ backupId: backup.backupId, title: "Compose Backup", diff --git a/packages/server/src/utils/backups/index.ts b/packages/server/src/utils/backups/index.ts index cd88539d5..c747c8656 100644 --- a/packages/server/src/utils/backups/index.ts +++ b/packages/server/src/utils/backups/index.ts @@ -1,4 +1,3 @@ -import path from "node:path"; import { CLEANUP_CRON_JOB } from "@dokploy/server/constants"; import { member } from "@dokploy/server/db/schema"; import type { BackupSchedule } from "@dokploy/server/services/backup"; @@ -11,7 +10,7 @@ import { startLogCleanup } from "../access-log/handler"; import { cleanupAll } from "../docker/utils"; import { sendDockerCleanupNotifications } from "../notifications/docker-cleanup"; import { execAsync, execAsyncRemote } from "../process/execAsync"; -import { getS3Credentials, scheduleBackup } from "./utils"; +import { getS3Credentials, normalizeS3Path, scheduleBackup } from "./utils"; export const initCronJobs = async () => { console.log("Setting up cron jobs...."); @@ -117,18 +116,15 @@ export const keepLatestNBackups = async ( try { const rcloneFlags = getS3Credentials(backup.destination); - const backupFilesPath = path.join( - `:s3:${backup.destination.bucket}`, - backup.prefix, - ); + const backupFilesPath = `:s3:${backup.destination.bucket}/${backup.appName}/${normalizeS3Path(backup.prefix)}`; // --include "*.sql.gz" or "*.zip" ensures nothing else other than the dokploy backup files are touched by rclone const rcloneList = `rclone lsf ${rcloneFlags.join(" ")} --include "*${backup.databaseType === "web-server" ? ".zip" : ".sql.gz"}" ${backupFilesPath}`; // when we pipe the above command with this one, we only get the list of files we want to delete const sortAndPickUnwantedBackups = `sort -r | tail -n +$((${backup.keepLatestCount}+1)) | xargs -I{}`; // this command deletes the files - // to test the deletion before actually deleting we can add --dry-run before ${backupFilesPath}/{} - const rcloneDelete = `rclone delete ${rcloneFlags.join(" ")} ${backupFilesPath}/{}`; + // to test the deletion before actually deleting we can add --dry-run before ${backupFilesPath}{} + const rcloneDelete = `rclone delete ${rcloneFlags.join(" ")} ${backupFilesPath}{}`; const rcloneCommand = `${rcloneList} | ${sortAndPickUnwantedBackups} ${rcloneDelete}`; diff --git a/packages/server/src/utils/backups/mariadb.ts b/packages/server/src/utils/backups/mariadb.ts index 56cb1a9aa..292b08cc8 100644 --- a/packages/server/src/utils/backups/mariadb.ts +++ b/packages/server/src/utils/backups/mariadb.ts @@ -20,7 +20,7 @@ export const runMariadbBackup = async ( const { prefix } = backup; const destination = backup.destination; const backupFileName = `${new Date().toISOString()}.sql.gz`; - const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`; + const bucketDestination = `${backup.appName}/${normalizeS3Path(prefix)}${backupFileName}`; const deployment = await createDeploymentBackup({ backupId: backup.backupId, title: "MariaDB Backup", diff --git a/packages/server/src/utils/backups/mongo.ts b/packages/server/src/utils/backups/mongo.ts index 2071478a0..10a434560 100644 --- a/packages/server/src/utils/backups/mongo.ts +++ b/packages/server/src/utils/backups/mongo.ts @@ -17,7 +17,7 @@ export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => { const { prefix } = backup; const destination = backup.destination; const backupFileName = `${new Date().toISOString()}.sql.gz`; - const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`; + const bucketDestination = `${backup.appName}/${normalizeS3Path(prefix)}${backupFileName}`; const deployment = await createDeploymentBackup({ backupId: backup.backupId, title: "MongoDB Backup", diff --git a/packages/server/src/utils/backups/mysql.ts b/packages/server/src/utils/backups/mysql.ts index d131090fa..26c3105a4 100644 --- a/packages/server/src/utils/backups/mysql.ts +++ b/packages/server/src/utils/backups/mysql.ts @@ -17,7 +17,7 @@ export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => { const { prefix } = backup; const destination = backup.destination; const backupFileName = `${new Date().toISOString()}.sql.gz`; - const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`; + const bucketDestination = `${backup.appName}/${normalizeS3Path(prefix)}${backupFileName}`; const deployment = await createDeploymentBackup({ backupId: backup.backupId, title: "MySQL Backup", diff --git a/packages/server/src/utils/backups/postgres.ts b/packages/server/src/utils/backups/postgres.ts index 9241f2103..de493f0bd 100644 --- a/packages/server/src/utils/backups/postgres.ts +++ b/packages/server/src/utils/backups/postgres.ts @@ -26,7 +26,7 @@ export const runPostgresBackup = async ( const { prefix } = backup; const destination = backup.destination; const backupFileName = `${new Date().toISOString()}.sql.gz`; - const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`; + const bucketDestination = `${backup.appName}/${normalizeS3Path(prefix)}${backupFileName}`; try { const rcloneFlags = getS3Credentials(destination); const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`; diff --git a/packages/server/src/utils/backups/web-server.ts b/packages/server/src/utils/backups/web-server.ts index 84f6b863a..1a51d23ea 100644 --- a/packages/server/src/utils/backups/web-server.ts +++ b/packages/server/src/utils/backups/web-server.ts @@ -31,7 +31,7 @@ export const runWebServerBackup = async (backup: BackupSchedule) => { const { BASE_PATH } = paths(); const tempDir = await mkdtemp(join(tmpdir(), "dokploy-backup-")); const backupFileName = `webserver-backup-${timestamp}.zip`; - const s3Path = `:s3:${destination.bucket}/${normalizeS3Path(backup.prefix)}${backupFileName}`; + const s3Path = `:s3:${destination.bucket}/${backup.appName}/${normalizeS3Path(backup.prefix)}${backupFileName}`; try { await execAsync(`mkdir -p ${tempDir}/filesystem`); diff --git a/packages/server/src/utils/volume-backups/backup.ts b/packages/server/src/utils/volume-backups/backup.ts index ad3eea29f..dd1575563 100644 --- a/packages/server/src/utils/volume-backups/backup.ts +++ b/packages/server/src/utils/volume-backups/backup.ts @@ -13,10 +13,7 @@ export const backupVolume = async ( const { VOLUME_BACKUPS_PATH, VOLUME_BACKUP_LOCK_PATH } = paths(!!serverId); const destination = volumeBackup.destination; const backupFileName = `${volumeName}-${new Date().toISOString()}.tar`; - const effectivePrefix = prefix - ? normalizeS3Path(prefix) - : `${volumeBackup.appName}/`; - const bucketDestination = `${effectivePrefix}${backupFileName}`; + const bucketDestination = `${volumeBackup.appName}/${normalizeS3Path(prefix || "")}${backupFileName}`; const rcloneFlags = getS3Credentials(volumeBackup.destination); const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`; const volumeBackupPath = path.join(VOLUME_BACKUPS_PATH, volumeBackup.appName); diff --git a/packages/server/src/utils/volume-backups/utils.ts b/packages/server/src/utils/volume-backups/utils.ts index d179dd096..dc2a4504f 100644 --- a/packages/server/src/utils/volume-backups/utils.ts +++ b/packages/server/src/utils/volume-backups/utils.ts @@ -81,11 +81,8 @@ const cleanupOldVolumeBackups = async ( try { const rcloneFlags = getS3Credentials(destination); - const normalizedPrefix = prefix - ? normalizeS3Path(prefix) - : `${volumeBackup.appName}/`; - const backupFilesPath = `:s3:${destination.bucket}/${normalizedPrefix}`; - const listCommand = `rclone lsf ${rcloneFlags.join(" ")} --include \"${volumeName}-*.tar\" :s3:${destination.bucket}/${normalizedPrefix}`; + const backupFilesPath = `:s3:${destination.bucket}/${volumeBackup.appName}/${normalizeS3Path(prefix || "")}`; + const listCommand = `rclone lsf ${rcloneFlags.join(" ")} --include \"${volumeName}-*.tar\" ${backupFilesPath}`; const sortAndPick = `sort -r | tail -n +$((${keepLatestCount}+1)) | xargs -I{}`; const deleteCommand = `rclone delete ${rcloneFlags.join(" ")} ${backupFilesPath}{}`; const fullCommand = `${listCommand} | ${sortAndPick} ${deleteCommand}`; From 50182a8048bcf623bc5f7b9f18c4071c89920803 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sat, 7 Mar 2026 02:32:47 -0600 Subject: [PATCH 18/30] fix: prevent doubled /v1/ suffix in Azure OpenAI-compatible URLs --- packages/server/src/utils/ai/select-ai-provider.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/server/src/utils/ai/select-ai-provider.ts b/packages/server/src/utils/ai/select-ai-provider.ts index 08f6b7383..b6477c573 100644 --- a/packages/server/src/utils/ai/select-ai-provider.ts +++ b/packages/server/src/utils/ai/select-ai-provider.ts @@ -30,6 +30,18 @@ export function selectAIProvider(config: { apiUrl: string; apiKey: string }) { baseURL: config.apiUrl, }); case "azure": + // Azure OpenAI-compatible endpoints already include /v1 in the path. + // Using createAzure with such URLs causes a doubled /v1//v1/ suffix. + if (config.apiUrl.includes("/v1")) { + return createOpenAICompatible({ + name: "azure", + baseURL: config.apiUrl, + headers: { + "api-key": config.apiKey, + Authorization: `Bearer ${config.apiKey}`, + }, + }); + } return createAzure({ apiKey: config.apiKey, baseURL: config.apiUrl, From 4882bd25ad0f32a532b50afd08987a4dc29535ea Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sat, 7 Mar 2026 02:38:29 -0600 Subject: [PATCH 19/30] feat: include backup file in restoreComposeBackup function for improved restore process --- packages/server/src/utils/restore/compose.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/server/src/utils/restore/compose.ts b/packages/server/src/utils/restore/compose.ts index d55b12fd8..10797a51d 100644 --- a/packages/server/src/utils/restore/compose.ts +++ b/packages/server/src/utils/restore/compose.ts @@ -69,6 +69,7 @@ export const restoreComposeBackup = async ( }, restoreType: composeType, rcloneCommand, + backupFile: backupInput.backupFile, }); emit("Starting restore..."); From 076262e47981699bad7cd296afee78f7225eade0 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sat, 7 Mar 2026 17:44:01 -0600 Subject: [PATCH 20/30] feat: add maxAliasCount option to parse function for improved Docker Compose file handling --- packages/server/src/utils/docker/compose.ts | 4 +++- packages/server/src/utils/docker/domain.ts | 8 ++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/server/src/utils/docker/compose.ts b/packages/server/src/utils/docker/compose.ts index a78b416ec..2e2011b03 100644 --- a/packages/server/src/utils/docker/compose.ts +++ b/packages/server/src/utils/docker/compose.ts @@ -18,7 +18,9 @@ export const randomizeComposeFile = async ( ) => { const compose = await findComposeById(composeId); const composeFile = compose.composeFile; - const composeData = parse(composeFile) as ComposeSpecification; + const composeData = parse(composeFile, { + maxAliasCount: 10000, + }) as ComposeSpecification; const randomSuffix = suffix || generateRandomHash(); diff --git a/packages/server/src/utils/docker/domain.ts b/packages/server/src/utils/docker/domain.ts index 230453e56..a9a425a93 100644 --- a/packages/server/src/utils/docker/domain.ts +++ b/packages/server/src/utils/docker/domain.ts @@ -63,7 +63,9 @@ export const loadDockerCompose = async ( if (existsSync(path)) { const yamlStr = readFileSync(path, "utf8"); - const parsedConfig = parse(yamlStr) as ComposeSpecification; + const parsedConfig = parse(yamlStr, { + maxAliasCount: 10000, + }) as ComposeSpecification; return parsedConfig; } return null; @@ -86,7 +88,9 @@ export const loadDockerComposeRemote = async ( return null; } if (!stdout) return null; - const parsedConfig = parse(stdout) as ComposeSpecification; + const parsedConfig = parse(stdout, { + maxAliasCount: 10000, + }) as ComposeSpecification; return parsedConfig; } catch { return null; From a8467e80e86b9b69917c4dac62d3af0e32737ca2 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sat, 7 Mar 2026 18:02:25 -0600 Subject: [PATCH 21/30] refactor: replace authClient with api.user.session.useQuery in multiple components for improved session management --- .../components/dashboard/search-command.tsx | 3 +-- .../git/github/add-github-provider.tsx | 8 ++++---- .../dashboard/settings/users/show-users.tsx | 3 +-- apps/dokploy/components/layouts/side.tsx | 2 +- .../pages/api/providers/github/setup.ts | 19 +++++++++++++------ apps/dokploy/server/api/routers/user.ts | 10 ++++++++++ 6 files changed, 30 insertions(+), 15 deletions(-) diff --git a/apps/dokploy/components/dashboard/search-command.tsx b/apps/dokploy/components/dashboard/search-command.tsx index bbd612d92..b98099b5f 100644 --- a/apps/dokploy/components/dashboard/search-command.tsx +++ b/apps/dokploy/components/dashboard/search-command.tsx @@ -23,7 +23,6 @@ import { CommandList, CommandSeparator, } from "@/components/ui/command"; -import { authClient } from "@/lib/auth-client"; import { api } from "@/utils/api"; import { StatusTooltip } from "../shared/status-tooltip"; @@ -56,7 +55,7 @@ export const SearchCommand = () => { const router = useRouter(); const [open, setOpen] = React.useState(false); const [search, setSearch] = React.useState(""); - const { data: session } = authClient.useSession(); + const { data: session } = api.user.session.useQuery(); const { data } = api.project.all.useQuery(undefined, { enabled: !!session, }); diff --git a/apps/dokploy/components/dashboard/settings/git/github/add-github-provider.tsx b/apps/dokploy/components/dashboard/settings/git/github/add-github-provider.tsx index d29b3a345..60fe2d343 100644 --- a/apps/dokploy/components/dashboard/settings/git/github/add-github-provider.tsx +++ b/apps/dokploy/components/dashboard/settings/git/github/add-github-provider.tsx @@ -12,14 +12,14 @@ import { } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; -import { authClient } from "@/lib/auth-client"; import { api } from "@/utils/api"; export const AddGithubProvider = () => { const [isOpen, setIsOpen] = useState(false); const { data: activeOrganization } = api.organization.active.useQuery(); - const { data: session } = authClient.useSession(); + const { data: session } = api.user.session.useQuery(); + console.log(session); const { data } = api.user.get.useQuery(); const [manifest, setManifest] = useState(""); const [isOrganization, setIsOrganization] = useState(false); @@ -99,8 +99,8 @@ export const AddGithubProvider = () => {
diff --git a/apps/dokploy/components/dashboard/settings/users/show-users.tsx b/apps/dokploy/components/dashboard/settings/users/show-users.tsx index 0245739f8..56f029664 100644 --- a/apps/dokploy/components/dashboard/settings/users/show-users.tsx +++ b/apps/dokploy/components/dashboard/settings/users/show-users.tsx @@ -26,7 +26,6 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; -import { authClient } from "@/lib/auth-client"; import { api } from "@/utils/api"; import { AddUserPermissions } from "./add-permissions"; import { ChangeRole } from "./change-role"; @@ -37,7 +36,7 @@ export const ShowUsers = () => { const { mutateAsync } = api.user.remove.useMutation(); const utils = api.useUtils(); - const { data: session } = authClient.useSession(); + const { data: session } = api.user.session.useQuery(); return (
diff --git a/apps/dokploy/components/layouts/side.tsx b/apps/dokploy/components/layouts/side.tsx index f34c33a31..6dea37f5b 100644 --- a/apps/dokploy/components/layouts/side.tsx +++ b/apps/dokploy/components/layouts/side.tsx @@ -546,7 +546,7 @@ function SidebarLogo() { const { state } = useSidebar(); const { data: isCloud } = api.settings.isCloud.useQuery(); const { data: user } = api.user.get.useQuery(); - const { data: session } = authClient.useSession(); + const { data: session } = api.user.session.useQuery(); const { data: organizations, refetch, diff --git a/apps/dokploy/pages/api/providers/github/setup.ts b/apps/dokploy/pages/api/providers/github/setup.ts index c09d6fba5..663939c5e 100644 --- a/apps/dokploy/pages/api/providers/github/setup.ts +++ b/apps/dokploy/pages/api/providers/github/setup.ts @@ -10,22 +10,29 @@ type Query = { state: string; installation_id: string; setup_action: string; - userId: string; }; export default async function handler( req: NextApiRequest, res: NextApiResponse, ) { - const { code, state, installation_id, userId }: Query = req.query as Query; + const { code, state, installation_id }: Query = req.query as Query; if (!code) { return res.status(400).json({ error: "Missing code parameter" }); } - const [action, value] = state?.split(":"); - // Value could be the organizationId or the githubProviderId + const [action, ...rest] = state?.split(":"); + // For gh_init: rest[0] = organizationId, rest[1] = userId + // For gh_setup: rest[0] = githubProviderId if (action === "gh_init") { + const organizationId = rest[0]; + const userId = rest[1] || (req.query.userId as string); + + if (!userId) { + return res.status(400).json({ error: "Missing userId parameter" }); + } + const octokit = new Octokit({}); const { data } = await octokit.request( "POST /app-manifests/{code}/conversions", @@ -44,7 +51,7 @@ export default async function handler( githubWebhookSecret: data.webhook_secret, githubPrivateKey: data.pem, }, - value as string, + organizationId as string, userId, ); } else if (action === "gh_setup") { @@ -53,7 +60,7 @@ export default async function handler( .set({ githubInstallationId: installation_id, }) - .where(eq(github.githubId, value as string)) + .where(eq(github.githubId, rest[0] as string)) .returning(); } diff --git a/apps/dokploy/server/api/routers/user.ts b/apps/dokploy/server/api/routers/user.ts index 3f217ceed..f67b62925 100644 --- a/apps/dokploy/server/api/routers/user.ts +++ b/apps/dokploy/server/api/routers/user.ts @@ -101,6 +101,16 @@ export const userRouter = createTRPCRouter({ return memberResult; }), + session: protectedProcedure.query(async ({ ctx }) => { + return { + user: { + id: ctx.user.id, + }, + session: { + activeOrganizationId: ctx.session.activeOrganizationId, + }, + }; + }), get: protectedProcedure.query(async ({ ctx }) => { const memberResult = await db.query.member.findFirst({ where: and( From 21821295e3d7f17b7d97567b78dfbc27dcc6a8d3 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sat, 7 Mar 2026 18:10:35 -0600 Subject: [PATCH 22/30] chore: remove console.log for session in AddGithubProvider component to clean up code --- .../dashboard/settings/git/github/add-github-provider.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/dokploy/components/dashboard/settings/git/github/add-github-provider.tsx b/apps/dokploy/components/dashboard/settings/git/github/add-github-provider.tsx index 60fe2d343..f2ba167ff 100644 --- a/apps/dokploy/components/dashboard/settings/git/github/add-github-provider.tsx +++ b/apps/dokploy/components/dashboard/settings/git/github/add-github-provider.tsx @@ -19,7 +19,6 @@ export const AddGithubProvider = () => { const { data: activeOrganization } = api.organization.active.useQuery(); const { data: session } = api.user.session.useQuery(); - console.log(session); const { data } = api.user.get.useQuery(); const [manifest, setManifest] = useState(""); const [isOrganization, setIsOrganization] = useState(false); From 735c9952d8bca5b357ad033d091e7698c54c6eae Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sat, 7 Mar 2026 18:14:30 -0600 Subject: [PATCH 23/30] chore: import authClient in show-users component for enhanced authentication handling --- apps/dokploy/components/dashboard/settings/users/show-users.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/dokploy/components/dashboard/settings/users/show-users.tsx b/apps/dokploy/components/dashboard/settings/users/show-users.tsx index 56f029664..f4bc9b897 100644 --- a/apps/dokploy/components/dashboard/settings/users/show-users.tsx +++ b/apps/dokploy/components/dashboard/settings/users/show-users.tsx @@ -26,6 +26,7 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; +import { authClient } from "@/lib/auth-client"; import { api } from "@/utils/api"; import { AddUserPermissions } from "./add-permissions"; import { ChangeRole } from "./change-role"; From 922b4d58f18994a65641641b2abaa69c50e35383 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sat, 7 Mar 2026 23:32:41 -0600 Subject: [PATCH 24/30] refactor: enhance backup functionality by incorporating appName and serviceName for S3 bucket paths --- packages/server/src/utils/backups/compose.ts | 7 ++++--- packages/server/src/utils/backups/index.ts | 17 ++++++++++++++- packages/server/src/utils/backups/mariadb.ts | 4 ++-- packages/server/src/utils/backups/mongo.ts | 4 ++-- packages/server/src/utils/backups/mysql.ts | 4 ++-- packages/server/src/utils/backups/postgres.ts | 4 ++-- .../server/src/utils/volume-backups/backup.ts | 21 ++++++++++++++++++- .../server/src/utils/volume-backups/utils.ts | 5 +++-- 8 files changed, 51 insertions(+), 15 deletions(-) diff --git a/packages/server/src/utils/backups/compose.ts b/packages/server/src/utils/backups/compose.ts index 28124f809..34f6d2a9b 100644 --- a/packages/server/src/utils/backups/compose.ts +++ b/packages/server/src/utils/backups/compose.ts @@ -14,13 +14,14 @@ export const runComposeBackup = async ( compose: Compose, backup: BackupSchedule, ) => { - const { environmentId, name } = compose; + const { environmentId, name, appName } = compose; const environment = await findEnvironmentById(environmentId); const project = await findProjectById(environment.projectId); - const { prefix, databaseType } = backup; + const { prefix, databaseType, serviceName } = backup; const destination = backup.destination; const backupFileName = `${new Date().toISOString()}.sql.gz`; - const bucketDestination = `${backup.appName}/${normalizeS3Path(prefix)}${backupFileName}`; + const s3AppName = serviceName ? `${appName}_${serviceName}` : appName; + const bucketDestination = `${s3AppName}/${normalizeS3Path(prefix)}${backupFileName}`; const deployment = await createDeploymentBackup({ backupId: backup.backupId, title: "Compose Backup", diff --git a/packages/server/src/utils/backups/index.ts b/packages/server/src/utils/backups/index.ts index c747c8656..71eeda7ea 100644 --- a/packages/server/src/utils/backups/index.ts +++ b/packages/server/src/utils/backups/index.ts @@ -106,6 +106,20 @@ export const initCronJobs = async () => { } }; +const getServiceAppName = (backup: BackupSchedule): string => { + if (backup.compose?.appName) { + return backup.serviceName + ? `${backup.compose.appName}_${backup.serviceName}` + : backup.compose.appName; + } + const serviceAppName = + backup.postgres?.appName || + backup.mysql?.appName || + backup.mariadb?.appName || + backup.mongo?.appName; + return serviceAppName || backup.appName; +}; + export const keepLatestNBackups = async ( backup: BackupSchedule, serverId?: string | null, @@ -116,7 +130,8 @@ export const keepLatestNBackups = async ( try { const rcloneFlags = getS3Credentials(backup.destination); - const backupFilesPath = `:s3:${backup.destination.bucket}/${backup.appName}/${normalizeS3Path(backup.prefix)}`; + const appName = getServiceAppName(backup); + const backupFilesPath = `:s3:${backup.destination.bucket}/${appName}/${normalizeS3Path(backup.prefix)}`; // --include "*.sql.gz" or "*.zip" ensures nothing else other than the dokploy backup files are touched by rclone const rcloneList = `rclone lsf ${rcloneFlags.join(" ")} --include "*${backup.databaseType === "web-server" ? ".zip" : ".sql.gz"}" ${backupFilesPath}`; diff --git a/packages/server/src/utils/backups/mariadb.ts b/packages/server/src/utils/backups/mariadb.ts index 292b08cc8..089b3cb04 100644 --- a/packages/server/src/utils/backups/mariadb.ts +++ b/packages/server/src/utils/backups/mariadb.ts @@ -14,13 +14,13 @@ export const runMariadbBackup = async ( mariadb: Mariadb, backup: BackupSchedule, ) => { - const { environmentId, name } = mariadb; + const { environmentId, name, appName } = mariadb; const environment = await findEnvironmentById(environmentId); const project = await findProjectById(environment.projectId); const { prefix } = backup; const destination = backup.destination; const backupFileName = `${new Date().toISOString()}.sql.gz`; - const bucketDestination = `${backup.appName}/${normalizeS3Path(prefix)}${backupFileName}`; + const bucketDestination = `${appName}/${normalizeS3Path(prefix)}${backupFileName}`; const deployment = await createDeploymentBackup({ backupId: backup.backupId, title: "MariaDB Backup", diff --git a/packages/server/src/utils/backups/mongo.ts b/packages/server/src/utils/backups/mongo.ts index 10a434560..d1b04e68b 100644 --- a/packages/server/src/utils/backups/mongo.ts +++ b/packages/server/src/utils/backups/mongo.ts @@ -11,13 +11,13 @@ import { execAsync, execAsyncRemote } from "../process/execAsync"; import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils"; export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => { - const { environmentId, name } = mongo; + const { environmentId, name, appName } = mongo; const environment = await findEnvironmentById(environmentId); const project = await findProjectById(environment.projectId); const { prefix } = backup; const destination = backup.destination; const backupFileName = `${new Date().toISOString()}.sql.gz`; - const bucketDestination = `${backup.appName}/${normalizeS3Path(prefix)}${backupFileName}`; + const bucketDestination = `${appName}/${normalizeS3Path(prefix)}${backupFileName}`; const deployment = await createDeploymentBackup({ backupId: backup.backupId, title: "MongoDB Backup", diff --git a/packages/server/src/utils/backups/mysql.ts b/packages/server/src/utils/backups/mysql.ts index 26c3105a4..461a17bf9 100644 --- a/packages/server/src/utils/backups/mysql.ts +++ b/packages/server/src/utils/backups/mysql.ts @@ -11,13 +11,13 @@ import { execAsync, execAsyncRemote } from "../process/execAsync"; import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils"; export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => { - const { environmentId, name } = mysql; + const { environmentId, name, appName } = mysql; const environment = await findEnvironmentById(environmentId); const project = await findProjectById(environment.projectId); const { prefix } = backup; const destination = backup.destination; const backupFileName = `${new Date().toISOString()}.sql.gz`; - const bucketDestination = `${backup.appName}/${normalizeS3Path(prefix)}${backupFileName}`; + const bucketDestination = `${appName}/${normalizeS3Path(prefix)}${backupFileName}`; const deployment = await createDeploymentBackup({ backupId: backup.backupId, title: "MySQL Backup", diff --git a/packages/server/src/utils/backups/postgres.ts b/packages/server/src/utils/backups/postgres.ts index de493f0bd..3371b0cf9 100644 --- a/packages/server/src/utils/backups/postgres.ts +++ b/packages/server/src/utils/backups/postgres.ts @@ -14,7 +14,7 @@ export const runPostgresBackup = async ( postgres: Postgres, backup: BackupSchedule, ) => { - const { name, environmentId } = postgres; + const { name, environmentId, appName } = postgres; const environment = await findEnvironmentById(environmentId); const project = await findProjectById(environment.projectId); @@ -26,7 +26,7 @@ export const runPostgresBackup = async ( const { prefix } = backup; const destination = backup.destination; const backupFileName = `${new Date().toISOString()}.sql.gz`; - const bucketDestination = `${backup.appName}/${normalizeS3Path(prefix)}${backupFileName}`; + const bucketDestination = `${appName}/${normalizeS3Path(prefix)}${backupFileName}`; try { const rcloneFlags = getS3Credentials(destination); const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`; diff --git a/packages/server/src/utils/volume-backups/backup.ts b/packages/server/src/utils/volume-backups/backup.ts index dd1575563..e192fd698 100644 --- a/packages/server/src/utils/volume-backups/backup.ts +++ b/packages/server/src/utils/volume-backups/backup.ts @@ -4,6 +4,24 @@ import { findComposeById } from "@dokploy/server/services/compose"; import type { findVolumeBackupById } from "@dokploy/server/services/volume-backups"; import { getS3Credentials, normalizeS3Path } from "../backups/utils"; +export const getVolumeServiceAppName = ( + volumeBackup: Awaited>, +): string => { + if (volumeBackup.compose?.appName) { + return volumeBackup.serviceName + ? `${volumeBackup.compose.appName}_${volumeBackup.serviceName}` + : volumeBackup.compose.appName; + } + const serviceAppName = + volumeBackup.application?.appName || + volumeBackup.postgres?.appName || + volumeBackup.mysql?.appName || + volumeBackup.mariadb?.appName || + volumeBackup.mongo?.appName || + volumeBackup.redis?.appName; + return serviceAppName || volumeBackup.appName; +}; + export const backupVolume = async ( volumeBackup: Awaited>, ) => { @@ -12,8 +30,9 @@ export const backupVolume = async ( volumeBackup.application?.serverId || volumeBackup.compose?.serverId; const { VOLUME_BACKUPS_PATH, VOLUME_BACKUP_LOCK_PATH } = paths(!!serverId); const destination = volumeBackup.destination; + const s3AppName = getVolumeServiceAppName(volumeBackup); const backupFileName = `${volumeName}-${new Date().toISOString()}.tar`; - const bucketDestination = `${volumeBackup.appName}/${normalizeS3Path(prefix || "")}${backupFileName}`; + const bucketDestination = `${s3AppName}/${normalizeS3Path(prefix || "")}${backupFileName}`; const rcloneFlags = getS3Credentials(volumeBackup.destination); const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`; const volumeBackupPath = path.join(VOLUME_BACKUPS_PATH, volumeBackup.appName); diff --git a/packages/server/src/utils/volume-backups/utils.ts b/packages/server/src/utils/volume-backups/utils.ts index dc2a4504f..6a51e765d 100644 --- a/packages/server/src/utils/volume-backups/utils.ts +++ b/packages/server/src/utils/volume-backups/utils.ts @@ -12,7 +12,7 @@ import { import { scheduledJobs, scheduleJob } from "node-schedule"; import { getS3Credentials, normalizeS3Path } from "../backups/utils"; import { sendVolumeBackupNotifications } from "../notifications/volume-backup"; -import { backupVolume } from "./backup"; +import { backupVolume, getVolumeServiceAppName } from "./backup"; // Helper functions to extract project info from volume backup const getProjectName = ( @@ -81,7 +81,8 @@ const cleanupOldVolumeBackups = async ( try { const rcloneFlags = getS3Credentials(destination); - const backupFilesPath = `:s3:${destination.bucket}/${volumeBackup.appName}/${normalizeS3Path(prefix || "")}`; + const s3AppName = getVolumeServiceAppName(volumeBackup); + const backupFilesPath = `:s3:${destination.bucket}/${s3AppName}/${normalizeS3Path(prefix || "")}`; const listCommand = `rclone lsf ${rcloneFlags.join(" ")} --include \"${volumeName}-*.tar\" ${backupFilesPath}`; const sortAndPick = `sort -r | tail -n +$((${keepLatestCount}+1)) | xargs -I{}`; const deleteCommand = `rclone delete ${rcloneFlags.join(" ")} ${backupFilesPath}{}`; From b419294b0968b2799587a45896d0ee0770429c57 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sat, 7 Mar 2026 23:38:58 -0600 Subject: [PATCH 25/30] fix: add --drop option to mongorestore command for improved data restoration https://github.com/Dokploy/dokploy/issues/2713 --- packages/server/src/utils/restore/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/utils/restore/utils.ts b/packages/server/src/utils/restore/utils.ts index 23052e642..7300ca479 100644 --- a/packages/server/src/utils/restore/utils.ts +++ b/packages/server/src/utils/restore/utils.ts @@ -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 --drop"`; }; export const getComposeSearchCommand = ( From b4319c7ea25941a805fd0887c2d3598f3485ee2b Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 8 Mar 2026 02:46:55 -0600 Subject: [PATCH 26/30] Bump version from v0.28.4 to v0.28.5 --- 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 7ed8a2e6a..1acc5d4bb 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -1,6 +1,6 @@ { "name": "dokploy", - "version": "v0.28.4", + "version": "v0.28.5", "private": true, "license": "Apache-2.0", "type": "module", From 75a4e8e8ef34569e1c5c60f23706ce5db0eb0a17 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sun, 8 Mar 2026 02:52:46 -0600 Subject: [PATCH 27/30] fix: update success message for service deployment to reflect queued status --- .../project/[projectId]/environment/[environmentId].tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx index 07a7396e2..de1b94b72 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx @@ -777,7 +777,7 @@ const EnvironmentPage = ( } if (success > 0) { toast.success( - `${success} service${success !== 1 ? "s" : ""} deployed successfully`, + `${success} service${success !== 1 ? "s" : ""} queued for deployment`, ); } if (failed > 0) { From ce82e2322b8e721cee0988f7c7fa953acf7e7b09 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sun, 8 Mar 2026 03:08:38 -0600 Subject: [PATCH 28/30] fix: improve port conflict detection by enhancing error messages and adding host-level service checks --- apps/dokploy/server/api/routers/settings.ts | 6 ++-- packages/server/src/services/settings.ts | 39 ++++++++++++++++----- 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts index fee7f2f5d..30cb522ba 100644 --- a/apps/dokploy/server/api/routers/settings.ts +++ b/apps/dokploy/server/api/routers/settings.ts @@ -149,12 +149,12 @@ export const settingsRouter = createTRPCRouter({ // Check if port 8080 is already in use before enabling dashboard const portCheck = await checkPortInUse(8080, input.serverId); if (portCheck.isInUse) { - const conflictingContainer = portCheck.conflictingContainer - ? ` by container "${portCheck.conflictingContainer}"` + const conflictInfo = portCheck.conflictingContainer + ? ` by ${portCheck.conflictingContainer}` : ""; throw new TRPCError({ code: "CONFLICT", - message: `Port 8080 is already in use${conflictingContainer}. Please stop the conflicting service or use a different port for the Traefik dashboard.`, + message: `Port 8080 is already in use${conflictInfo}. Please stop the conflicting service or use a different port for the Traefik dashboard.`, }); } newPorts.push({ diff --git a/packages/server/src/services/settings.ts b/packages/server/src/services/settings.ts index f3603a8f0..07aaf690c 100644 --- a/packages/server/src/services/settings.ts +++ b/packages/server/src/services/settings.ts @@ -413,17 +413,38 @@ export const checkPortInUse = async ( serverId?: string, ): Promise<{ isInUse: boolean; conflictingContainer?: string }> => { try { - const command = `docker ps -a --format '{{.Names}}' | grep -v '^dokploy-traefik$' | while read name; do docker port "$name" 2>/dev/null | grep -q ':${port}' && echo "$name" && break; done || true`; - const { stdout } = serverId - ? await execAsyncRemote(serverId, command) - : await execAsync(command); + // Check if port is in use by a Docker container + const dockerCommand = `docker ps -a --format '{{.Names}}' | grep -v '^dokploy-traefik$' | while read name; do docker port "$name" 2>/dev/null | grep -q ':${port}' && echo "$name" && break; done || true`; + const { stdout: dockerOut } = serverId + ? await execAsyncRemote(serverId, dockerCommand) + : await execAsync(dockerCommand); - const container = stdout.trim(); + const container = dockerOut.trim(); - return { - isInUse: !!container, - conflictingContainer: container || undefined, - }; + if (container) { + return { + isInUse: true, + conflictingContainer: `container "${container}"`, + }; + } + + // Check if port is in use by a host-level service (non-Docker) + // Dokploy runs inside a container, so we spawn an ephemeral container + // with --net=host to share the host's network stack and use nc -z to + // check if something is listening on the port + const hostCommand = `docker run --rm --net=host busybox sh -c 'nc -z 0.0.0.0 ${port} 2>/dev/null && echo in_use || echo free'`; + const { stdout: hostOut } = serverId + ? await execAsyncRemote(serverId, hostCommand) + : await execAsync(hostCommand); + + if (hostOut.includes("in_use")) { + return { + isInUse: true, + conflictingContainer: "a host-level service", + }; + } + + return { isInUse: false }; } catch (error) { console.error("Error checking port availability:", error); return { isInUse: false }; From c00aa6acbf585b125badf4a992abaf19350f96db Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sun, 8 Mar 2026 16:16:45 -0600 Subject: [PATCH 29/30] fix: enhance container metrics query to support wildcard matching for container names --- apps/monitoring/database/containers.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/monitoring/database/containers.go b/apps/monitoring/database/containers.go index 6dad7f5ea..43ff468a6 100644 --- a/apps/monitoring/database/containers.go +++ b/apps/monitoring/database/containers.go @@ -54,13 +54,13 @@ func (db *DB) GetLastNContainerMetrics(containerName string, limit int) ([]Conta WITH recent_metrics AS ( SELECT metrics_json FROM container_metrics - WHERE container_name = ? + WHERE container_name = ? OR container_name LIKE ? ORDER BY timestamp DESC LIMIT ? ) SELECT metrics_json FROM recent_metrics ORDER BY json_extract(metrics_json, '$.timestamp') ASC ` - rows, err := db.Query(query, containerName, limit) + rows, err := db.Query(query, containerName, containerName+".%", limit) if err != nil { return nil, err } @@ -90,12 +90,12 @@ func (db *DB) GetAllMetricsContainer(containerName string) ([]ContainerMetric, e WITH recent_metrics AS ( SELECT metrics_json FROM container_metrics - WHERE container_name = ? + WHERE container_name = ? OR container_name LIKE ? ORDER BY timestamp DESC ) SELECT metrics_json FROM recent_metrics ORDER BY json_extract(metrics_json, '$.timestamp') ASC ` - rows, err := db.Query(query, containerName) + rows, err := db.Query(query, containerName, containerName+".%") if err != nil { return nil, err } From 2102840bb969356b85c3c8643366bf5b6701fdff Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sun, 8 Mar 2026 23:48:51 -0600 Subject: [PATCH 30/30] fix: add error handling to trusted origins retrieval in admin service --- packages/server/src/services/admin.ts | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/server/src/services/admin.ts b/packages/server/src/services/admin.ts index 2721777df..f0c8cb0eb 100644 --- a/packages/server/src/services/admin.ts +++ b/packages/server/src/services/admin.ts @@ -135,15 +135,25 @@ export const getTrustedOrigins = async () => { if (trustedOriginsCache && now < trustedOriginsCache.expiresAt) { return trustedOriginsCache.data; } - const trustedOrigins = await runQuery(); - trustedOriginsCache = { - data: trustedOrigins, - expiresAt: now + TRUSTED_ORIGINS_CACHE_TTL_MS, - }; - return trustedOrigins; + try { + const trustedOrigins = await runQuery(); + trustedOriginsCache = { + data: trustedOrigins, + expiresAt: now + TRUSTED_ORIGINS_CACHE_TTL_MS, + }; + return trustedOrigins; + } catch (error) { + console.error("Failed to fetch trusted origins:", error); + return trustedOriginsCache?.data ?? []; + } } - return runQuery(); + try { + return await runQuery(); + } catch (error) { + console.error("Failed to fetch trusted origins:", error); + return []; + } }; export const getTrustedProviders = async () => {