← Dev log

devops

A backup you haven't restored is a hypothesis

A backup you haven't restored is a hypothesis

The backup job here is unremarkable: pg_dump -Fc out of the running database container every night, a local copy kept on a retention window, a copy shipped to offsite object storage over the S3 API, both pruned past the window. Offsite shipping is mandatory by default — the script refuses to run without the offsite config unless you explicitly override it, because a backup that only exists on the same disk as the database is barely a backup.

That part is easy. The part that's easy to skip is proving any of it works.

The separate job that actually matters

A second workflow, on its own schedule, downloads the most recent offsite backup, restores it into a disposable throwaway database container — never the live one — and asserts that a couple of core tables come back with at least one row. Then it tears the container down.

This is the job that tells you something true. pg_dump exiting zero tells you a file was written. It tells you nothing about whether pg_restore will accept that file, whether the schema round-trips, whether a custom type or extension is missing on the restore side, whether the offsite copy uploaded intact. I've seen restore scripts that had been "working" for months break the first time they were actually run, because nobody had run them.

Details that bit

  • The restore script runs pg_restore --clean --if-exists and drops and recreates the target schema. Point it at a throwaway database name by config, always. Rehearsing a restore against the live database because you fat-fingered an env var is a self-inflicted incident.
  • Mark the database volume external: true in the production compose file, pinned to its exact existing name. Compose then refuses to delete a volume it doesn't own — so a down -v against the wrong file, or a future project-naming mistake, can't take the data with it. This does not protect against a manual docker volume prune; nothing at the config layer fully does. The only real protection there is the rule: no --volumes pruning, ever, on a box where the database container could be momentarily stopped.
  • Make the backup cron itself alert on failure. Mine used to fail silently to a log file nobody read. It now posts to a chat channel on failure, with self-contained calls so it doesn't depend on a repo checkout being present.