Skip to content

fix(backups): scope retention to the files of its own database - #4917

Open
joaquinleondev wants to merge 1 commit into
Dokploy:canaryfrom
joaquinleondev:fix/backup-retention-per-database
Open

fix(backups): scope retention to the files of its own database#4917
joaquinleondev wants to merge 1 commit into
Dokploy:canaryfrom
joaquinleondev:fix/backup-retention-per-database

Conversation

@joaquinleondev

Copy link
Copy Markdown

Closes #4914

Problem

A backup's object key is <service-appName>/<prefix>/<timestamp>.sql.gz. appName identifies the service, not the database, and the file name is only a timestamp — so two backups of two databases hosted by the same Postgres/MySQL/Mongo service land in the same folder under names that carry no identity.

keepLatestNBackups then lists the whole folder and deletes everything past its own keepLatestCount:

rclone lsf … --include "*.{sql.gz,bson.gz}" :s3:bucket/<appName>/<prefix>
  | sort -r | tail -n +$((keepLatestCount+1)) | xargs -I{} rclone delete … {}

The list is not scoped to the backup that owns it, so the smallest keepLatestCount among the backups sharing the folder silently governs all of them. In my case a daily backup of a 9.4 GB database with Keep latest = 7 ended up holding a single file, because a second database in the same service had a weekly backup with Keep latest = 2. Both backups reported ✅ and the UI kept showing "7".

What changed

  • Backup files are now named <database>-<timestamp>.sql.gz (.bson.gz for mongo), via a new getBackupFileName helper. This mirrors what web server backups already do (webserver-backup-<timestamp>.zip) and, as a bonus, makes a shared folder readable by a human.
  • Retention's --include is built from the same getBackupFilePrefix, so it only ever lists the files of the database it runs for.
  • The command building moved into an exported getKeepLatestNBackupsCommand(backup, destination) so it can be tested without hitting the database or S3; keepLatestNBackups is otherwise unchanged.
  • Web server backups keep the plain *.zip pattern: their folder is keyed by the backup's own appName, so they cannot collide with anything.

Upgrade note, worth calling out: files written before this change are named <timestamp>.sql.gz, do not match the new pattern, and therefore stop being pruned automatically — they have to be deleted once, by hand. Any fix that makes retention distinguish its own files has this property; the alternative (keeping the old pattern in the filter) would preserve the bug. Happy to add a follow-up that also prunes legacy files when a folder holds a single backup, if you think it's worth the extra query.

Testing

Added apps/dokploy/__test__/backups/retention-scope.test.ts. It runs the command produced by getKeepLatestNBackupsCommand for real, with an rclone stub on PATH that honours --include (rclone's {a,b} alternation translated to bash extglob) and records what rclone delete was asked to remove. Given a folder holding three app-* dumps and two analytics-* dumps:

  • retention for app with keep=2 deletes only the oldest app-* file;
  • retention for analytics with keep=1 deletes only the oldest analytics-* file and never touches app-*;
  • retention deletes nothing when the database has fewer files than keepLatestCount;
  • web server backups still match *.zip;
  • plus unit tests for the file name and for the sanitising of the prefix (the database name ends up both in an S3 key and inside an rclone glob, so anything outside [a-zA-Z0-9._-] becomes _).
✓ __test__/backups/retention-scope.test.ts (6 tests) 33ms
Test Files  1 passed (1)
     Tests  6 passed (6)

I also checked the tests fail against the current behaviour: restoring the old --include "*.{sql.gz,bson.gz}" pattern makes exactly the two cross-database cases fail.

Full suite, before and after, on the same machine (the failures are pre-existing and environmental — they need a database and a swarm manager, neither of which exists in my sandbox):

test files tests
canary 42 failed / 29 passed 4 failed / 267 passed / 1 skipped
this branch 42 failed / 30 passed 4 failed / 273 passed / 1 skipped

biome check clean on the touched files, tsc --noEmit clean in packages/server.

The object key of a backup is <service-appName>/<prefix>/<timestamp>.gz,
so two backups of two databases hosted by the same service land in the
same folder under names that carry no identity. keepLatestNBackups then
lists that whole folder and deletes everything past its own
keepLatestCount, which means the smallest keepLatestCount among the
backups sharing the folder silently governs all of them: a daily backup
set to keep 7 ends up with 1 file because a weekly backup of a second
database in the same service keeps 2.

Backup files are now named <database>-<timestamp>.gz and retention only
lists that database's files, so a backup can no longer delete another
one's dumps. Web server backups keep their pattern: they already live in
a folder of their own, keyed by the backup's appName.

Note for existing installations: files written before this change do not
match the new pattern and are no longer pruned automatically; they have
to be removed once, by hand.

Closes Dokploy#4914
@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. bug Something isn't working labels Jul 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Backup retention deletes other backups' files when two backups share a database service

1 participant