Backup & restore
One thing to back up: the PostgreSQL database. Losing Redis loses nothing durable; losing PostgreSQL loses the fleet.
What backs up
pg_dump in custom format (-Fc) against the same
schema-owning connection string Alembic uses for migrations - not a separate
backup credential. Custom format means a restore can be selective
(pg_restore -t <table>) rather than always a full replay.
backup-portrait.shwrites a timestamped.dumpto the backup directory (default/opt/portrait/backups) atomically - via a.partialrename - so a run killed mid-dump can never look like a complete backup. It prunes anything older than the retention window (default 14 days).restore-portrait.sh <dump-file>restores into a test database (portrait_test) by default. It never targets the live database unless you explicitly tell it to.- A systemd timer runs the backup daily at 03:30 local time, with
Persistent=true- a run missed because the host was asleep fires as soon as the timer is next evaluated.
Installing the backup job
These scripts and units are version-controlled references, applied by hand - Portrait’s packaging does not install timers automatically yet:
sudo cp packaging/postgresql/backup-portrait.sh packaging/postgresql/restore-portrait.sh /opt/portrait/ sudo chown portrait:portrait /opt/portrait/backup-portrait.sh /opt/portrait/restore-portrait.sh sudo chmod +x /opt/portrait/backup-portrait.sh /opt/portrait/restore-portrait.sh sudo cp packaging/systemd/portrait-backup.service packaging/systemd/portrait-backup.timer /etc/systemd/system/ sudo systemctl daemon-reload sudo systemctl enable --now portrait-backup.timer
The restore drill
Do this periodically, not only when something breaks:
sudo -u portrait /opt/portrait/restore-portrait.sh /opt/portrait/backups/portrait-<timestamp>.dump
This restores into portrait_test, never the live database. Confirm row
counts and spot-check a few records against the live database. If
portrait_test does not exist yet, either run the test suite once (it creates
it as a side effect) or have someone with PostgreSQL admin rights run
CREATE DATABASE portrait_test OWNER portrait; once by hand - the
restore script deliberately cannot create databases itself.
Restoring over production
This is destructive and needs a human decision each time, so it is not a flag on the default path. Stop the writers first, then restore with the live database as the explicit target:
sudo systemctl stop portrait-api portrait-alert-scheduler portrait-profile-scheduler PORTRAIT_RESTORE_DB=portrait sudo -u portrait -E /opt/portrait/restore-portrait.sh /opt/portrait/backups/portrait-<timestamp>.dump sudo systemctl start portrait-api portrait-alert-scheduler portrait-profile-scheduler
What this does not give you
Point-in-time recovery. A pg_dump approach means you can lose up to a
day of activity between backups. This is a deliberate v1 trade - it needs no
changes to the PostgreSQL configuration (no WAL archiving target to provision and
secure) and matches the “runs at the floor” sizing rule. If a real incident
ever makes a day of loss unacceptable, continuous WAL archiving is the thing to add.