How backups work on thoreau and how to restore.
See also:
The scheme in one picture
daily 03:00 (btrbk.timer)
│
├─ services-predump.service pg_dump each DB → /srv/services/postgres/dumps/
│ │ (must finish first — btrbk waits on it, not on a timer)
▼ ▼
btrbk.service
├─ snapshot @services (NVMe) → send to @backup (HDD raid1) ← disaster copy
└─ snapshot @data (HDD) → local only ← undo button
@services→@backup: the disaster copy. Survives loss of the NVMe. Each snapshot contains the raw service files and the SQL dumps, so it’s restore-ready even if the raw Postgres files won’t cleanly recover.@datalocal snapshots: the “undo button” for accidental deletion or overwrite. Fast, local, no network. Does not protect against losing the box.
Retention: @services keeps 7 daily + 5 weekly + 3 monthly (~90 days). @data keeps 7 daily + 5 weekly (~35 days — covers a month-long absence).
Not yet covered
- Off-site: everything currently lives on thoreau. A fire / theft / dual-disk failure loses all of it. Off-site replication of
@backup,@data, and@mediais still to be built — the biggest open gap, especially given month-long unattended periods.@media(Paperless / Immich) has no backup in this scheme — excluded as high-churn bulk. Its protection is meant to be off-site + app-level trash. Until off-site exists,@mediais unprotected.- Restore has not been tested end-to-end. Do a throwaway restore (below) before trusting these backups with real stakes.
Important Files
| Path | What |
|---|---|
/etc/btrbk/btrbk.conf | btrbk config (sources, targets, retention) |
/usr/local/bin/services-predump.sh | DB dump script |
/etc/systemd/system/services-predump.service | dump unit |
/etc/systemd/system/btrbk.service | btrbk run (chained after dump) |
/etc/systemd/system/btrbk.timer | daily 03:00 trigger |
/srv/services/postgres/dumps/ | latest SQL dumps |
/store/backup/@backup/ | received @services snapshots (disaster copy) |
/mnt/btrbk/{srv,store} | tool-only fs-root mounts btrbk operates through |
Routine operations
Run a backup now (dump → snapshot → send):
sudo systemctl start btrbk.service
journalctl -u services-predump.service -u btrbk.service --since "5 min ago"Dry run (plan only, no changes):
sudo btrbk -c /etc/btrbk/btrbk.conf -n runList snapshots / backups:
sudo btrbk -c /etc/btrbk/btrbk.conf list snapshots
sudo btrfs subvolume list /mnt/btrbk/store | grep @backupCheck the timer:
systemctl list-timers btrbk.timerRestore — undo an accidental deletion in @data
Local snapshots are directly readable. Copy the file back out:
ls /mnt/btrbk/store/btrbk-snapshots/ # find the dated snapshot
cp /mnt/btrbk/store/btrbk-snapshots/@data.<DATE>/path/to/file /store/data/path/to/fileNo btrbk needed — snapshots are just read-only subvolumes.
Restore — a single database
From the dump inside the latest snapshot (or the live dumps dir):
# forgejo example — stop the app first so nothing writes mid-restore
cd /srv/docker && docker compose stop forgejo
docker exec -i selfhosted-postgres-1 psql -U postgres forgejo \
< /srv/services/postgres/dumps/forgejo.sql
docker compose start forgejoThe dumps use --clean --if-exists, so they drop and recreate objects — safe to re-run into an existing database.
Restore — full services after losing the NVMe
-
Reinstall/replace the system disk; recreate the
@servicessubvolume and its mount (see thoreau architecture). -
Send the latest
@servicessnapshot back from the HDD copy:btrfs send /mnt/btrbk/store/@backup/@services.<DATE> \ | btrfs receive /mnt/btrbk/srv/# then position/rename it as the live @services mount -
If the raw Postgres files won’t start cleanly, restore roles then each DB from the dumps carried inside that snapshot:
docker exec -i selfhosted-postgres-1 psql -U postgres < .../globals.sqldocker exec -i selfhosted-postgres-1 psql -U postgres forgejo < .../forgejo.sqldocker exec -i selfhosted-postgres-1 psql -U postgres paperless < .../paperless.sqldocker exec -i selfhosted-postgres-1 psql -U postgres vaultwarden < .../vaultwarden.sql -
docker compose up -d, then verify per thoreau Maintenance health checks.
Test a restore
Spin up a throwaway Postgres and load a dump. This proves the dumps are valid without touching production:
docker run --rm -d --name pgtest -e POSTGRES_PASSWORD=test postgres:16-alpine
docker exec -i pgtest psql -U postgres < /srv/services/postgres/dumps/globals.sql
docker exec -i pgtest psql -U postgres -c 'CREATE DATABASE forgejo;'
docker exec -i pgtest psql -U postgres forgejo < /srv/services/postgres/dumps/forgejo.sql
docker exec pgtest psql -U postgres forgejo -c 'SELECT count(*) FROM repository;' # sanity
docker stop pgtestA sensible row count means the dump can restore. This is the only way to know the backups work for real