feat(backup): dump before migrating, and restore a dump into an empty database

The image now carries postgresql18-client (pg_dump and pg_restore 18.6)
and a /var/lib/healthlog/backups directory owned by the app user, which
the bundled compose file backs with a new `backups` volume for `app` and
the commented `app-worker`; a fresh named volume inherits that owner.

Before `migrate deploy`, when migrations are pending on a database that
has applied some, the entrypoint writes healthlog-<time>-pre-migration.dump
with its manifest and keeps the last two. A failed dump on a writable
directory stops the boot and names DB_BACKUP_BEFORE_MIGRATE=false; without
a writable directory it warns and continues.

`docker compose run --rm db-restore <file|latest|list>` runs
scripts/db-restore.mjs in the app image with plain node. It opens an
off-host .dump.enc with BACKUP_ENCRYPTION_KEY (or a previous key), checks
the archive against the manifest's SHA-256, compares the keys in the
environment with the manifest's fingerprints and refuses on a different
active encryption key unless --ignore-key-mismatch, refuses a database
that holds any table, restores in one transaction, runs ANALYZE, and
compares every table's row count with the manifest, emptying the database
again on a difference.

DB_BACKUP_ENABLED, DB_BACKUP_DIR, DB_BACKUP_SCHEDULE, DB_BACKUP_KEEP and
DB_BACKUP_BEFORE_MIGRATE join the compose whitelist, both example files
and the env manifest.
This commit is contained in:
Marc-André Bombeck committed 2026-10-25 12:56:33 +01:00
1 parent 6d3a1582fa
commit e665f17f52
11 files changed
+830 -15

No files matched your search

@@ -58,7 +58,9 @@ describe("the Postgres 16 -> 18 move in docker-compose.yml", () => {
it("keeps the new data where the Postgres 18 image expects it", () => {
// Mounted at .../data, as 16 had it, the 18 image refuses to start.
expect(db.volumes).toEqual(["postgres-data:/var/lib/postgresql"]);
// `backups` holds the app's database dumps, not database files.
expect(Object.keys(compose.volumes).sort()).toEqual([
"backups",
"pgdata",
"postgres-data",
]);