diff --git a/apps/dokploy/__test__/backups/web-server-filters.test.ts b/apps/dokploy/__test__/backups/web-server-filters.test.ts new file mode 100644 index 00000000000..a9c923bc041 --- /dev/null +++ b/apps/dokploy/__test__/backups/web-server-filters.test.ts @@ -0,0 +1,67 @@ +import { execSync } from "node:child_process"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { WEB_SERVER_BACKUP_EXCLUDES } from "@dokploy/server/utils/backups/web-server-filters"; +import { afterEach, describe, expect, it } from "vitest"; + +const hasRsync = (() => { + try { + execSync("rsync --version", { stdio: "ignore" }); + return true; + } catch { + return false; + } +})(); + +let root: string | undefined; +afterEach(() => { + if (root) rmSync(root, { recursive: true, force: true }); + root = undefined; +}); + +const backedUpFiles = (files: string[]) => { + root = mkdtempSync(join(tmpdir(), "web-server-filters-")); + const src = join(root, "src"); + const dst = join(root, "dst"); + mkdirSync(dst); + for (const file of files) { + mkdirSync(dirname(join(src, file)), { recursive: true }); + writeFileSync(join(src, file), "x"); + } + execSync(`rsync -a ${WEB_SERVER_BACKUP_EXCLUDES.join(" ")} ${src}/ ${dst}/`); + return execSync("find . -type f", { cwd: dst }) + .toString() + .trim() + .split("\n") + .map((file) => file.replace(/^\.\//, "")) + .sort(); +}; + +describe("WEB_SERVER_BACKUP_EXCLUDES", () => { + it.skipIf(!hasRsync)( + "skips cloned .git history but keeps working trees and bind-mounted data", + () => { + const files = backedUpFiles([ + "applications/git-app/code/.git/HEAD", + "applications/git-app/code/.git/objects/pack/big.pack", + "applications/git-app/code/src/index.ts", + "applications/drop-app/code/index.html", + "compose/stack/code/.git/HEAD", + "compose/stack/code/docker-compose.yml", + "compose/stack/code/data/PG_VERSION", + "compose/stack/files/config.yml", + "traefik/traefik.yml", + ]); + + expect(files).toEqual([ + "applications/drop-app/code/index.html", + "applications/git-app/code/src/index.ts", + "compose/stack/code/data/PG_VERSION", + "compose/stack/code/docker-compose.yml", + "compose/stack/files/config.yml", + "traefik/traefik.yml", + ]); + }, + ); +}); diff --git a/packages/server/src/utils/backups/web-server-filters.ts b/packages/server/src/utils/backups/web-server-filters.ts new file mode 100644 index 00000000000..2f621a6e9b6 --- /dev/null +++ b/packages/server/src/utils/backups/web-server-filters.ts @@ -0,0 +1,7 @@ +// rsync excludes for the web-server backup. Every deploy re-clones */code/, +// so its .git history is never needed. The working tree stays in the backup: +// compose bind mounts like ./data live there, and restore replaces BASE_PATH. +export const WEB_SERVER_BACKUP_EXCLUDES = [ + "--exclude='applications/*/code/.git/'", + "--exclude='compose/*/code/.git/'", +]; diff --git a/packages/server/src/utils/backups/web-server.ts b/packages/server/src/utils/backups/web-server.ts index 01b03cc7af7..686cb0902cc 100644 --- a/packages/server/src/utils/backups/web-server.ts +++ b/packages/server/src/utils/backups/web-server.ts @@ -17,6 +17,7 @@ import { sendDokployBackupNotifications } from "../notifications/dokploy-backup" import { execAsync } from "../process/execAsync"; import { redactRcloneCredentials } from "./redact"; import { getBackupFileName, getS3Credentials, normalizeS3Path } from "./utils"; +import { WEB_SERVER_BACKUP_EXCLUDES } from "./web-server-filters"; function formatBytes(bytes?: number) { if (bytes === undefined) return "Unknown size"; @@ -85,7 +86,7 @@ export const runWebServerBackup = async (backup: BackupSchedule) => { await execAsync(cleanupCommand); await execAsync( - `rsync -a --ignore-errors --no-specials --no-devices --exclude='volume-backups/' --exclude='${ENCRYPTION_KEY_BACKUP_FILE}' ${BASE_PATH}/ ${tempDir}/filesystem/`, + `rsync -a --ignore-errors --no-specials --no-devices --exclude='volume-backups/' --exclude='${ENCRYPTION_KEY_BACKUP_FILE}' ${WEB_SERVER_BACKUP_EXCLUDES.join(" ")} ${BASE_PATH}/ ${tempDir}/filesystem/`, ); writeStream.write("Copied filesystem to temp directory\n");