From 6ea5a213ab00577ea776e7a4c26e0d02d41e14eb Mon Sep 17 00:00:00 2001 From: Firdaus Date: Sat, 24 Jan 2026 19:48:26 -0500 Subject: [PATCH 1/3] fix: Add ability to backup database on worker node --- packages/server/src/utils/backups/postgres.ts | 59 +++++++++++++++++-- packages/server/src/utils/backups/utils.ts | 55 ++++++++++++++++- 2 files changed, 108 insertions(+), 6 deletions(-) diff --git a/packages/server/src/utils/backups/postgres.ts b/packages/server/src/utils/backups/postgres.ts index 9241f2103a..adb8bab274 100644 --- a/packages/server/src/utils/backups/postgres.ts +++ b/packages/server/src/utils/backups/postgres.ts @@ -8,7 +8,16 @@ import type { Postgres } from "@dokploy/server/services/postgres"; import { findProjectById } from "@dokploy/server/services/project"; import { sendDatabaseBackupNotifications } from "../notifications/database-backup"; import { execAsync, execAsyncRemote } from "../process/execAsync"; -import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils"; +import { + getBackupCommand, + getContainerSearchCommand, + getCreateDatabaseBackupTempService, + getRemoveServiceCommand, + getS3Credentials, + getServiceExistsCommand, + getServiceNodeCommand, + normalizeS3Path, +} from "./utils"; export const runPostgresBackup = async ( postgres: Postgres, @@ -30,18 +39,58 @@ export const runPostgresBackup = async ( try { const rcloneFlags = getS3Credentials(destination); const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`; - const rcloneCommand = `rclone rcat ${rcloneFlags.join(" ")} "${rcloneDestination}"`; + const searchCommand = getContainerSearchCommand(backup); + + if (!searchCommand) { + throw new Error("searchCommand is empty"); + } + + const { stdout: serviceId } = await execAsync(searchCommand, { + shell: "/bin/bash", + }); const backupCommand = getBackupCommand( backup, rcloneCommand, deployment.logPath, ); - if (postgres.serverId) { - await execAsyncRemote(postgres.serverId, backupCommand); + + if (serviceId) { + if (postgres.serverId) { + await execAsyncRemote(postgres.serverId, backupCommand); + } else { + await execAsync(backupCommand, { + shell: "/bin/bash", + }); + } } else { - await execAsync(backupCommand, { + const serviceNodeCommend = getServiceNodeCommand(postgres.appName); + const { stdout: node } = await execAsync(serviceNodeCommend, { + shell: "/bin/bash", + }); + + const serviceExistCommand = getServiceExistsCommand(postgres.appName); + const { stdout: exist } = await execAsync(serviceExistCommand, { + shell: "/bin/bash", + }); + + if (exist.trim() === "true") { + const removeServiceCommand = getRemoveServiceCommand(postgres.appName); + await execAsync(removeServiceCommand, { + shell: "/bin/bash", + }); + } + + const createDatabaseBackupTempService = + getCreateDatabaseBackupTempService( + backup, + postgres.appName, + node, + rcloneCommand, + ); + + await execAsync(createDatabaseBackupTempService, { shell: "/bin/bash", }); } diff --git a/packages/server/src/utils/backups/utils.ts b/packages/server/src/utils/backups/utils.ts index f30577a53b..ddbad49089 100644 --- a/packages/server/src/utils/backups/utils.ts +++ b/packages/server/src/utils/backups/utils.ts @@ -122,7 +122,7 @@ export const getComposeContainerCommand = ( return `docker ps -q --filter "status=running" --filter "label=com.docker.compose.project=${appName}" --filter "label=com.docker.compose.service=${serviceName}" | head -n 1`; }; -const getContainerSearchCommand = (backup: BackupSchedule) => { +export const getContainerSearchCommand = (backup: BackupSchedule) => { const { backupType, postgres, mysql, mariadb, mongo, compose, serviceName } = backup; @@ -268,3 +268,56 @@ export const getBackupCommand = ( echo "Backup done ✅" >> ${logPath}; `; }; + +export const getServiceNodeCommand = (appName: string) => { + return `docker service ps ${appName} --format "{{.Node}}" --filter "desired-state=running"`; +}; + +export const getRemoveServiceCommand = (appName: string) => { + return `docker service rm backup-service-${appName}`; +}; + +export const getServiceExistsCommand = (appName: string) => { + return ` + if docker service ls \ + --filter name=backup-service-${appName} \ + --format '{{.Name}}' | \ + grep -q "^backup-service-${appName}$"; then + echo true + else + echo false + fi + `.trim(); +}; + +export const getCreateDatabaseBackupTempService = ( + backup: BackupSchedule, + appName: string, + node: string, + rcloneCommand: string, +) => { + const containerSearch = getContainerSearchCommand(backup); + const backupCommand = generateBackupCommand(backup); + + return [ + "docker service create", + `--name backup-service-${appName}`, + "--restart-condition none", + `--constraint 'node.hostname == ${node.trim()}'`, + "--mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock", + "alpine sh -c '", + "apk add --no-cache rclone docker-cli &&", + "set -eo pipefail;", + `CONTAINER_ID=$(${containerSearch});`, + 'if [ -z "$CONTAINER_ID" ]; then echo "❌ Error: Container not found"; exit 1; fi;', + 'echo "Container Up: $CONTAINER_ID";', + 'echo "Executing backup test...";', + `${backupCommand} > /dev/null || { echo "❌ Error: Backup process failed"; exit 1; };`, + 'echo "Starting upload to S3...";', + 'docker exec -i $CONTAINER_ID bash -c "set -o pipefail; pg_dump -Fc --no-acl --no-owner -h localhost -U postgres --no-password postgres | gzip" |', + `${rcloneCommand} || { echo "❌ Error: Upload to S3 failed"; exit 1; };`, + "sleep 6;", + 'echo "Backup done ✅"', + "'", + ].join(" "); +}; From 25e70569ec3ee121f280b2f7b18e993c54ef7f58 Mon Sep 17 00:00:00 2001 From: Firdaus Date: Sat, 24 Jan 2026 20:38:27 -0500 Subject: [PATCH 2/3] fix hardcoded database name --- packages/server/src/utils/backups/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/utils/backups/utils.ts b/packages/server/src/utils/backups/utils.ts index ddbad49089..7e3c4e1fe8 100644 --- a/packages/server/src/utils/backups/utils.ts +++ b/packages/server/src/utils/backups/utils.ts @@ -314,7 +314,7 @@ export const getCreateDatabaseBackupTempService = ( 'echo "Executing backup test...";', `${backupCommand} > /dev/null || { echo "❌ Error: Backup process failed"; exit 1; };`, 'echo "Starting upload to S3...";', - 'docker exec -i $CONTAINER_ID bash -c "set -o pipefail; pg_dump -Fc --no-acl --no-owner -h localhost -U postgres --no-password postgres | gzip" |', + `docker exec -i $CONTAINER_ID bash -c "set -o pipefail; pg_dump -Fc --no-acl --no-owner -h localhost -U postgres --no-password ${backup.database} | gzip" |`, `${rcloneCommand} || { echo "❌ Error: Upload to S3 failed"; exit 1; };`, "sleep 6;", 'echo "Backup done ✅"', From 69c54e47bc76cfd6cc7835460e4fb069980286ec Mon Sep 17 00:00:00 2001 From: Firdaus Date: Sat, 24 Jan 2026 20:49:39 -0500 Subject: [PATCH 3/3] fix: change harcoded database user --- packages/server/src/utils/backups/postgres.ts | 5 +++-- packages/server/src/utils/backups/utils.ts | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/server/src/utils/backups/postgres.ts b/packages/server/src/utils/backups/postgres.ts index adb8bab274..203d0475ed 100644 --- a/packages/server/src/utils/backups/postgres.ts +++ b/packages/server/src/utils/backups/postgres.ts @@ -23,7 +23,7 @@ export const runPostgresBackup = async ( postgres: Postgres, backup: BackupSchedule, ) => { - const { name, environmentId } = postgres; + const { name, environmentId, databaseUser, appName } = postgres; const environment = await findEnvironmentById(environmentId); const project = await findProjectById(environment.projectId); @@ -85,7 +85,8 @@ export const runPostgresBackup = async ( const createDatabaseBackupTempService = getCreateDatabaseBackupTempService( backup, - postgres.appName, + appName, + databaseUser, node, rcloneCommand, ); diff --git a/packages/server/src/utils/backups/utils.ts b/packages/server/src/utils/backups/utils.ts index 7e3c4e1fe8..f7ff50f018 100644 --- a/packages/server/src/utils/backups/utils.ts +++ b/packages/server/src/utils/backups/utils.ts @@ -293,6 +293,7 @@ export const getServiceExistsCommand = (appName: string) => { export const getCreateDatabaseBackupTempService = ( backup: BackupSchedule, appName: string, + databaseUser: string, node: string, rcloneCommand: string, ) => { @@ -314,7 +315,7 @@ export const getCreateDatabaseBackupTempService = ( 'echo "Executing backup test...";', `${backupCommand} > /dev/null || { echo "❌ Error: Backup process failed"; exit 1; };`, 'echo "Starting upload to S3...";', - `docker exec -i $CONTAINER_ID bash -c "set -o pipefail; pg_dump -Fc --no-acl --no-owner -h localhost -U postgres --no-password ${backup.database} | gzip" |`, + `docker exec -i $CONTAINER_ID bash -c "set -o pipefail; pg_dump -Fc --no-acl --no-owner -h localhost -U ${databaseUser} --no-password ${backup.database} | gzip" |`, `${rcloneCommand} || { echo "❌ Error: Upload to S3 failed"; exit 1; };`, "sleep 6;", 'echo "Backup done ✅"',