A database backup needs a consistent dump, a usable recovery format, and a restore test. This guide uses PostgreSQL custom-format archives and a separate PostgreSQL instance to check recovered content.
The old combined PostgreSQL/MySQL script is replaced with one explicit PostgreSQL workflow. MySQL and MariaDB need their own engine-specific procedures.
Verification status
| Area | Evidence and limits |
|---|---|
| Technical review | Official PostgreSQL and Docker references reviewed on October 31, 2026. |
| Static checks | Bash syntax and embedded Compose structure checked. |
| Runtime | Recovery lab run 41244956545 passed on October 31, 2026: dump, isolated restore, marker assertion, repeated-restore failure, and stopped-source backup failure. That run covered recovery only. |
| Scheduler | Scheduler lab run 41245987064 passed on October 31, 2026: manual service execution, accelerated timer activation, lock-contention failure, stopped-source failure, service result/exit status, and journal error details. Tested with systemd 255.4-1ubuntu8.18. The daily calendar was parsed; CI used a two-second timer override. |
| Tested versions | GitHub-hosted Ubuntu 24.04.5 amd64; Docker 28.0.4; Compose 2.38.2; PostgreSQL 16.15. Image digest: sha256:0cefaabd882e42bcb2a2f3dc56d3ce4770560ba62fd7f197f1a2e008fd5731ab. |
| Recovery chain | Integrated lab run 41247821482 passed on October 31, 2026: PostgreSQL dump → separately encrypted Restic copy → fresh PostgreSQL database. Original dumps, original repository, and original credentials were unavailable during recovery; restored archive bytes matched and partial files were excluded. Both repositories were local to CI. Restic 0.19.1 image digest: sha256:136600b6ff6843d61d355f7f71f460a166429f35de6fd11b568fece3c9a4d510. |
| Scope | Disposable PostgreSQL 16 lab, not a production deployment or point-in-time recovery solution. |
The repository includes an automated replay of this lab in tools/check-postgres-lab.py. It runs the article’s Bash examples on a disposable Docker host, checks recovery, rejects a repeated restore, and confirms a failed dump preserves existing archives. Check the PostgreSQL recovery lab Actions run for runtime evidence; merely adding the test does not establish a passing result. It also tests the generated systemd service, an accelerated timer, and the integrated Restic-to-database recovery chain on a disposable runner. Off-site recovery, external alert delivery, missed-run monitoring, downtime catch-up, and production data remain outside this test.
1. Prepare a separate lab
Use Bash, OpenSSL, Docker Engine, the Compose plugin, and host flock. This project publishes no database ports. Its two services have separate data volumes; only the test service is a restore target.
|
|
Use a Compose version supporting the wait flags and inspect logs if startup fails. Record image digests: the version-series tag can change.
The official image initializes the lab user on an empty volume. These commands use its local socket from inside the container. Production remote authentication and backup privileges need separate review. See the official image documentation.
2. Create a recognizable source record
|
|
This intentionally creates a new table in the disposable source. If you rerun it against the same lab, the existing-table error is expected; inspect the lab state rather than suppressing the error.
3. Write an atomic backup script
|
|
A dump or archive-list failure exits nonzero and leaves no published .dump file. The temporary file and final file share a filesystem, allowing publication by rename. Existing successful archives are retained.
The custom archive is already compressed by default and is read by pg_restore; it is not a gzip-compressed SQL script. Plain SQL requires a different restore command. See pg_dump.
The source database remains available during a logical dump. The dump covers one database, not cluster roles, tablespaces, application attachments, or a coordinated snapshot of multiple databases. Plan those separately.
4. Restore a chosen archive into the empty test instance
Run this once against the fresh restore_test database. Select the exact .dump filename printed by backup.sh:
|
|
Stop if the restore fails. Repeating it into a populated database is not an independent recovery test. Do not add clean/drop options or redirect it at source to bypass that problem.
Verify the marker using an assertion that fails on missing or incorrect data:
|
|
A successful marker test establishes this lab’s expected row, not an entire application’s correctness. Add schema, data, permissions, extension, and application checks for production. See pg_restore.
For plain SQL imports, use psql with ON_ERROR_STOP so SQL errors propagate. Do not hide stderr or treat “some tables exist” as a passed restore.
5. Schedule after the manual recovery test
Move the validated project to a stable host path before scheduling. Test backup.sh as the scheduler’s actual user, using the installed Docker executable and Compose plugin. Confirm its PATH, permissions, mount state, logs, and time zone.
The script already takes a nonblocking lock. Use a host timer or cron job to invoke it once per scheduled run; do not wrap it in a tight retry loop or rely on installing client tools every time a container starts.
Connect failure and missed-run alerts to the actual command result. Send success only after every required stage succeeds, including any required off-site transfer. A notification transport failure should be visible and should not turn a failed dump into success.
Keep retention separate until recovery is proven. Preview candidate archives, verify off-site availability and required recovery history, and scope deletion to this workflow. Do not delete older backups before confirming the new required copy succeeded.
Install a systemd service and daily timer
Run this section from a root Bash shell on a systemd host, inside the validated project at its stable path. Keep the same Compose project name when moving an existing deployment; changing it can select different volumes. The example assumes a local, root-managed Docker Engine and Python 3 for reading Compose’s resolved project name.
The path check keeps generated unit values simple. Use a stable path without spaces, percent signs, or shell metacharacters. Review any existing units with these names before replacing them.
|
|
The service uses the resolved Compose project explicitly and runs without your interactive shell environment. Root access to Docker is intentional here; these units require review before adapting to a rootless engine or a remote Docker context. All database commands must still target the intended project.
Test the service before enabling the timer:
|
|
Require Result=success, ExecMainStatus=0, and a new published archive. A completed oneshot normally becomes inactive; that alone is not a failure. This timer runs at 02:00 in the host’s time zone, plus up to 15 minutes of random delay. Check timedatectl. Persistent=true can trigger a catch-up after downtime; it does not replay every missed daily backup.
A nonzero script exit, including lock contention or a stopped source, marks the service failed and records stderr in the journal. Inspect Result, ExecMainStatus, and the journal before retrying. The timer being active proves only that it is waiting, not that backups are succeeding.
Use these signals in your monitoring system and separately alert on overdue successful archives. This example sends no email or webhook and cannot detect a powered-off host by itself. Avoid RemainAfterExit=yes: leaving this oneshot active prevents later timer activations from running it again. See systemd timers.
To stop this schedule while retaining the units and backups:
|
|
6. Add encrypted off-site recovery
Database dumps here are plaintext data in a compressed archive. File permissions restrict local access but are not encryption.
Use the Restic backup and restore guide to protect completed archives. Select only published .dump files, excluding .partial files. Validate the remote repository and restore an archive from that remote copy into another isolated instance.
Keep repository credentials, database versions, extension requirements, application secrets, and recovery instructions outside the original host. Measure recovery time; neither a successful upload nor a nonempty archive proves recoverability.
Logical dumps do not provide arbitrary point-in-time recovery. Review PostgreSQL backup guidance and WAL-based recovery requirements when the recovery target demands more than periodic snapshots.
Recover a database dump from a copied Restic repository
This extension joins the two labs. Both repositories are local to the disposable host; this tests the recovery chain, not off-site durability or a remote backend. Run it after successful dump creation. Stop the timer during this rehearsal so the selected dump directory does not change.
Use a reviewed official Restic image. The example below matches the Restic lab’s tested 0.19.1 release. Keep these passwords and the copied repository outside the original host in a real deployment.
|
|
Keep this staging directory limited to the workflow’s published dumps and temporary partial files. The exclude rule prevents a partially written dump from entering the snapshot. It does not make other arbitrary files safe to publish.
The recovery service has no mount for the original dumps, original repository, or its password. Recover from the copy and import a selected dump into the new database:
|
|
Require the same marker assertion against this new database:
|
|
Do not repeat this import into the populated database or point it at the source. The new database uses a fresh volume and its own bootstrap credential; production roles, extensions, attachments, and permissions still need their own recovery plan. Include both Compose files when stopping this extended lab.
Stop the lab
For the extended Restic lab, stop all its services with both files; otherwise use the original single-file command.
|
|
This retains the lab volumes and archives for inspection. Removing the disposable volumes is a separate decision after checking recovery evidence. No production database should be attached to this project.
Continue reading
- Compose health checks — Separate startup readiness from job success.
- Backup strategy — Define recovery targets and independent copies.