Restore
Use this procedure to restore Safe Harbor from a backup tarball created by
flask safeharbor backup.
The restore command replaces the PostgreSQL database and uploads directory with the contents of the selected tarball. Run the dry-run validation first, then stop the application before the destructive restore.
When to use this
Use this guide for three restore scenarios.
Disaster recovery
Use this when the Docker host died, the disk failed, or the original machine is
gone. Start from a fresh Safe Harbor deployment, place a known-good tarball in
./backups/, and restore it through the backups sidecar.
Point-in-time rollback
Use this when recent corruption means the whole application state must roll back to an earlier backup. This restores database rows and uploaded files from the selected point in time.
Dev refresh
Use this to refresh a local development database from a production backup.
Confirm the local .env points at the local database before running the
restore.
Pre-restore checklist
-
Stop the web app:
docker compose stop web worker -
Confirm
DATABASE_URLin your.envpoints at the database you intend to overwrite. -
Optional: take a fresh backup of current state first:
docker compose --profile backup exec backups flask safeharbor backup
Step 1: Validate the tarball with --dry-run
Run the dry-run before every restore:
Use the container path, not the host path
The --from value is resolved inside the backups container.
Use /backups/<filename>.tar (the container's bind-mount path),
not the host path like /Users/me/Source/Safe-Harbor/app/backups/<filename>.tar.
docker compose --profile backup exec backups flask safeharbor restore --from /backups/<filename>.tar --dry-run
Dry-run first
The dry-run checks that the tarball is readable, contains the expected
db.dump and uploads/ members, and can be inspected by pg_restore.
It does not overwrite the database or uploads.
A successful dry-run prints a summary like this:
would restore N tables, M upload files (total <size>) from <path>
Success means:
- The command exits with status
0. - The tarball structure passed validation.
pg_restore --listcould inspect the database dump.- No database rows or upload files changed.
If the dry-run fails, do not continue to the destructive restore. Re-check the filename, re-fetch the backup from your off-site copy, or restore a different tarball.
Step 2: Run the restore
After the dry-run succeeds and the app is stopped, run the restore:
docker compose --profile backup exec backups flask safeharbor restore --from /backups/<filename>.tar
This overwrites application state
The restore replaces the target database contents and upload files with
the contents of the selected backup tarball. Confirm .env points at the
database you intend to overwrite before you continue.
The command prompts before making destructive changes:
Type 'restore' to confirm — this WILL OVERWRITE the database and uploads:
Type exactly restore and press Enter to proceed:
restore
Any other input aborts the restore and leaves the destructive step unstarted.
For non-interactive use, such as a recovery script, pass --yes to skip the
prompt:
docker compose --profile backup exec backups flask safeharbor restore --from /backups/<filename>.tar --yes
Use --yes with care. It removes the final human confirmation before the
database and uploads are overwritten.
When the restore finishes, it prints
restored N tables and M upload files from /backups/<filename>.tar.
Step 3: Restart the app
Start the app containers again:
docker compose start web worker
Verify the restored state:
- Open the site.
- Log in.
- View a tank dashboard.
- Confirm measurements, animals, and uploaded images match the restored backup.
If something looks wrong, see Troubleshooting before attempting another restore.
Troubleshooting
Postgres version mismatch
If you migrated the host to a newer PostgreSQL major version since the backup,
pg_restore may fail. Restore into a fresh Postgres 16 instance, or downgrade
the host's Postgres version to match the backup source.
Partial restore after crash mid-process
pg_restore is transactional. If it crashed mid-restore, the database may be in
an inconsistent state. Drop the database and restore fresh from the tarball.
Corrupted tarball
Re-fetch the tarball from your off-site copy, such as a NAS or restic repository. The dry-run surfaces tarball corruption before the destructive restore step.
Manual recovery
Advanced users can unpack the tarball manually and restore each part by hand:
tar -xf <file>.tar
pg_restore --clean --if-exists --no-owner --no-acl -d <database> db.dump
cp -r uploads/* /data/uploads/
Use manual recovery only when the normal restore command is not available. Point
pg_restore at the correct database and copy uploads into the correct volume
path.
Drift caveats
Restoring an older backup into a newer schema can fail or produce a confused state if migrations were applied between the backup and now.
Use one of these approaches instead: restore into an empty database and re-apply
migrations forward, or run flask db downgrade before restoring a backup that
matches that older schema.
Do not assume the application will repair drift between an old backup and a newer partially migrated database.