Backup and recovery
Everything the system owns lives in two places:
| What | Where | Contains |
|---|---|---|
| Data folder | DATA_DIR (default ./data) |
deanreach.db (SQLite: contacts, history, settings), gmail-token.json, credentials.json, uploads/ (CVs) |
| Configuration | .env next to app.py |
admin login, secret key, paths, transport |
The source code is in your git repository; it does not need backing up separately.
Automatic backups
scripts/backup.sh writes a timestamped archive of the data folder using SQLite's online
backup command (safe while the app is running), keeps the last 30 archives, and prints the
restore command:
./scripts/backup.sh # -> backups/deanreach-2026-09-08T0200.tar.gz
Run it nightly:
crontab -e
0 2 * * * cd /opt/deanreach && ./scripts/backup.sh >> backups/backup.log 2>&1
Copy the backups/ folder off the machine (rsync to another host, a cloud bucket, or a
Google Drive sync folder). Whatever you already use for other files is fine; the archives
are small (a few MB).
With Docker Compose the same script runs on the host against ./data.
Restore
- Stop the app (
docker compose downorsystemctl stop deanreach). ./scripts/restore.sh backups/deanreach-2026-09-08T0200.tar.gz(it moves the current data folder aside asdata.before-restore-<time>first).- Start the app. Sign in, open the dashboard: contacts, history and settings are back. The Gmail connection is part of the data folder, so it comes back too.
Moving to a new server
Install on the new machine (Install and deploy), copy .env and the latest backup, run the
restore script, start. Update the OAuth redirect URI in Google Cloud if the host name changed.
PostgreSQL
If DATABASE_URL points at Postgres, back the database up with pg_dump on your usual
schedule and back up the data folder (token, CVs, client file) with the script above.
What to test twice a year
Restore a backup onto a laptop with TRANSPORT=dryrun and check that the contact count and
the latest history rows match production. A backup nobody has restored is a hope, not a
backup.