Restore a database backup into a throwaway environment, prove the data is usable, report the result. One command, bash and Docker, no daemon.
restore-check postgres --dump latest.pgdump --manifest ops/manifest.txt --check ops/checks.sql --notify slack
"An untested backup is not a backup" gets upvoted in every backup thread, and almost nobody automates the test. The teams that do write the same Lambda, Step Function, or nightly cron from scratch. This is that script, written once, with the parts that bite you already handled: throwaway instance, streamed restore, row-count manifest, custom checks, dated evidence file, notification, guaranteed teardown.
- Starts a fresh database in a container (or a temp file for SQLite).
- Restores your dump into it. Compressed dumps are streamed, not unpacked to disk.
- Validates:
- manifest: every listed table exists and has at least N rows. Catches truncated and stale dumps.
- checks: your SQL/JS files run against the restored data. Catches broken sequences, unreadable blobs, missing functions.
- min-tables: cheap guard against an empty restore.
- Prints a
key=valuereport, optionally saves it as audit evidence, optionally posts to Slack or a webhook. - Tears the instance down, even when a check fails or the job is killed.
Exit code tells the scheduler what happened:
| code | meaning |
|---|---|
| 0 | restore ok, all validations passed |
| 1 | restore ok, at least one validation failed |
| 2 | restore or environment failed |
| 3 | usage error |
git clone https://github.com/edubraqd/restore-check.git
ln -s "$PWD/restore-check/bin/restore-check" /usr/local/bin/restore-checkRequirements: bash 4.4+, Docker for the container engines, sqlite3 for the SQLite engine, curl for notifications. macOS users: brew install bash.
restore-check <engine> --dump PATH [options]
--manifest FILE "table min_rows" lines; every table must exist with >= min_rows
--check FILE check file executed against the restored instance (repeatable)
--min-tables N fail if fewer than N tables were restored
--report FILE write key=value report (audit evidence) to FILE
--notify BACKEND none | slack | webhook (default: none)
--image IMAGE override the engine's container image
--keep do not tear down the instance (for debugging)
Environment: RESTORE_CHECK_SLACK_WEBHOOK, RESTORE_CHECK_WEBHOOK_URL, RESTORE_CHECK_IMAGE, RESTORE_CHECK_READY_TIMEOUT.
# table min_rows (0 = must exist, may be empty)
public.users 1000
public.orders 25000
public.audit_log 0
Generate it from production so it tracks reality:
psql -tA -c "select schemaname||'.'||relname||' '||greatest(n_live_tup*9/10,0) from pg_stat_user_tables" > ops/manifest.txtA check file is run by the engine; it fails when the engine reports an error. For Postgres that means RAISE EXCEPTION, for MySQL an insert into a temp table with a CHECK constraint, for SQLite the same, for MongoDB throw. See examples/checks/postgres-smoke.sql for a sequence-vs-max(id) check, a freshness check, and a blob-readability check.
TIMESTAMP=2026-09-03T03:17:41Z
RESULT=PASS
ENGINE=postgres
ENGINE_INFO=image=postgres:16 db=restored
DUMP=/var/backups/app-2026-09-03.pgdump
DUMP_SHA256=9f3c...
DUMP_BYTES=734003200
TABLES=42
FAILED_CHECKS=0
DURATION_SECONDS=184
Keep these files. A folder of dated reports is what an auditor asks for.
| engine | input | status |
|---|---|---|
postgres |
pg_dump plain .sql, .sql.gz, custom/tar archive |
complete, e2e in CI |
mysql |
mysqldump .sql, .sql.gz (use --image mariadb:11 for MariaDB) |
complete, e2e in CI |
sqlite |
database file or .sql dump |
complete, e2e in CI, no Docker |
mongodb |
mongodump --archive, optionally --gzip |
basic, untested |
redis |
dump.rdb, optionally .gz |
basic, untested |
Adding one is a single file. See engines/README.md.
Ready-to-adapt examples in examples/: GitHub Actions nightly workflow, GitLab CI schedule, docker-compose, systemd timer, and an RDS snapshot flow for databases that cannot be dumped into a container.
- ...trust the managed provider? You are testing your ability to restore, not their ability to store bytes. Several people in the thread that prompted this had never seen what their provider's dump looks like.
- ...check that the backup job exited 0? A dump that completes fine can still have a truncated blob column or a sequence behind its table. Those only show up when something reads the data. That is what the checks are for.
- ...restore to staging? Fine, if staging is disposable and isolated. Most are neither. A container that dies at the end is.
bash tests/run.sh # unit tests with a fake engine, no Docker
bash tests/e2e-sqlite.sh # needs sqlite3
bash tests/e2e-postgres.sh # needs Docker
bash tests/e2e-mysql.sh # needs DockerMIT