Skip to content

Dump the database before applying pending migrations - #10

Merged
theNEXlevel merged 1 commit into
mainfrom
ops/pre-migration-backup
Aug 10, 2026
Merged

Dump the database before applying pending migrations#10
theNEXlevel merged 1 commit into
mainfrom
ops/pre-migration-backup

Conversation

@theNEXlevel

Copy link
Copy Markdown
Contributor

Why

prisma migrate deploy is forward-only, so a bad migration against the production database has no way back — and the deploy applies migrations automatically.

A template-backup service now runs pg_dump -Fc ahead of template-migrations, reusing the chaining the app already relies on:

template-db (healthy) → template-backup → template-migrations → template-app

Design notes

It uses the postgres:17 image, not the app image. pg_dump is therefore version-matched to the server by construction, and there's no client library to keep in sync. This matters: the equivalent change on va-partners initially shipped a client that silently could not authenticate to the database at all, because the auth plugin wasn't in the package. That failure mode is structurally impossible here.

required needs no scripting to enforce. template-migrations already waits on service_completed_successfully, so a non-zero backup stops the chain before any DDL runs. Compose does the gating.

BACKUP_MODE Behaviour
required (default) A failed dump blocks migrations — the app never starts
best-effort Dumps, but migrates anyway if the dump fails
off Never dumps

An unrecognised value fails rather than silently downgrading to a weaker mode.

Dumps are verified before they count. pg_restore --list must pass, and the file is moved into place only after that, so a truncated dump can never look like a good backup. Dumps land on the template-backups volume, pruned to the newest BACKUP_KEEP (default 10).

Scope

Deliberately simpler than the va-partners equivalent, which needed an entrypoint script because it had no separate migration service. No fingerprinting or crash-loop handling here — a one-shot service runs once per deploy, not on every container restart.

Verification

Against a real postgres:17:

  • Dump → pg_restore into a fresh database returns the rows intact, including UTF-8 and embedded apostrophes.
  • Full production-profile stack: backup exits 0, migrations exit 0, app healthy. Ordering confirmed by container timestamps — backup finished at 03:56:54.145, migrations started at 03:56:54.759.
  • Failed backup blocks the chain: with a bad BACKUP_MODE, compose up exits 1 and the migrations container is left created with StartedAt never set — it never executed.
  • off skips; best-effort continues past a failed dump; required exits 1; an invalid value exits 1.
  • Pruning holds at BACKUP_KEEP with no temp files left behind.
  • The exact pg_restore --list command added to CI was run as written.

Caveat worth carrying forward

These dumps sit on the same host and disk as template-db. They protect against a bad migration, not against losing the machine. Called out in the README so projects generated from this template don't mistake it for disaster recovery.

🤖 Generated with Claude Code

`prisma migrate deploy` is forward-only, so a bad migration against the
production database had no way back. A `template-backup` service now runs
`pg_dump -Fc` ahead of `template-migrations`, reusing the chaining the app
already relies on:

  template-db (healthy) -> template-backup -> template-migrations -> template-app

It runs the `postgres:17` image rather than the app image, so pg_dump is
version-matched to the server by construction and there is no client library
to keep in sync — the failure mode that a mismatched client causes is
structurally impossible here.

BACKUP_MODE is off | best-effort | required (default). `required` needs no
scripting to enforce: template-migrations waits on
service_completed_successfully, so a non-zero backup stops the chain before
any DDL runs. An unrecognised value fails rather than silently downgrading.

Dumps are verified with `pg_restore --list` and moved into place only after
passing, so a truncated file can never look like a good backup. They land on
the template-backups volume, pruned to the newest BACKUP_KEEP (default 10).

The publish smoke test now asserts the backup exited 0, reported a completed
dump, and that the dump passes `pg_restore --list`.

Verified against postgres:17: dump/restore round-trip preserves data and
utf8, a failed backup leaves template-migrations created-but-never-started,
off/best-effort/required and an invalid value all behave, and pruning holds
at BACKUP_KEEP with no temp files left behind.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@theNEXlevel
theNEXlevel merged commit e483c4a into main Aug 10, 2026
1 check passed
@theNEXlevel
theNEXlevel deleted the ops/pre-migration-backup branch August 10, 2026 04:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant