PurrOSDocs
Operations

Backups & upgrades

Built-in consistent backups, off-site copies, restoring, and upgrading safely.

What to back up

DataWhereBack up?
DatabasePostgreSQL (db service or managed Postgres)Yes, daily at least. PurrOS does this itself (see below).
Attachments$PURROS_STATE_DIR/files (the state volume in Docker), or your S3 bucketYes. With local storage, PurrOS backups include them.
config/, especially PURROS_SECRETYour serverYes, stored separately and securely
Redisredis serviceNo. It only holds rate-limit counters.

Without PURROS_SECRET, encrypted values in a restored database can't be read. These are integration settings, webhook secrets and authenticator-app secrets. purros backup inspect shows whether a backup matches the current secret.

Built-in backups

PurrOS makes its own database backups, so neither pg_dump nor an external tool is needed.

  • Consistent while running. Every table is read in a single snapshot, so the backup matches one moment even while people keep working.
  • One file. Each backup is a compressed .purros-backup file. It contains every table and a manifest with the PurrOS and schema version, row counts and a SHA-256 checksum per table.
  • Uploaded files included. With local file storage, backups also contain every attachment (PURROS_BACKUP_FILES=auto; set true to include them from S3 storage too, or false to leave them out). Restoring puts them back into the configured storage.
  • Off-site copies. With PURROS_BACKUP_S3_ENABLED, every backup is also uploaded to an S3-compatible bucket, keeping PURROS_BACKUP_S3_KEEP there. See Database backups to S3.
  • Optional encryption. Backups can be encrypted with a passphrase. The key is derived with Argon2id and the data is sealed with AES-256-GCM, so a damaged, edited or cut-off file is detected rather than restored.
  • Files are private. They are written with permissions 600, and appear only once complete.

On demand

docker compose exec api purros backup create
docker compose exec api purros backup create --encrypt          # asks for a passphrase
docker compose exec api purros backup list

Scheduled

Set PURROS_BACKUP_DIR, PURROS_BACKUP_S3_ENABLED or both, and the worker makes one backup a day and deletes old ones:

VariableDefaultDescription
PURROS_BACKUP_DIR(off); /var/lib/purros/backups in the config written by purros initWhere backups are written
PURROS_BACKUP_HOUR2Hour of the day (UTC) after which the daily backup runs
PURROS_BACKUP_KEEP14How many backups to keep. The newest is never deleted.
PURROS_BACKUP_PASSPHRASEEncrypt backups with this passphrase. Store it with PURROS_SECRET.
PURROS_BACKUP_FILESautoInclude uploaded files: auto (with local storage), true, false
PURROS_BACKUP_S3_ENABLED, PURROS_BACKUP_S3_*falseAlso upload to an S3 bucket (settings). Without PURROS_BACKUP_DIR, backups are only kept in the bucket.
PURROS_BACKUP_S3_KEEPPURROS_BACKUP_KEEPHow many backups to keep in the bucket

When several workers run, only one makes the backup. purros status shows the schedule and the last backup, and purros doctor fails if scheduled backups haven't succeeded for 36 hours.

The Compose file keeps backups in the state volume, under /var/lib/purros/backups. Keep a copy off the server: turn on S3 uploads, or copy the volume with your usual tools, for example:

docker compose cp api:/var/lib/purros/backups ./purros-backups

For very large installs or point-in-time recovery, add WAL archiving (for example pgBackRest or WAL-G) or your managed database's snapshots.

Attachment backups

  • Local storage: PurrOS backups include the files by default. For a separate copy, back up the state volume (or $PURROS_STATE_DIR/files) with your usual tools (restic, borg, rsync).
  • S3-compatible storage: turn on bucket versioning, and replicate to a second region or provider.

Restoring

docker compose stop api
docker compose run --rm api backup verify /var/lib/purros/backups/purros-20260927-020012-scheduled.purros-backup
docker compose run --rm api backup restore /var/lib/purros/backups/purros-20260927-020012-scheduled.purros-backup --replace
# or straight from the S3 backup bucket:
# docker compose run --rm api backup list --remote
# docker compose run --rm api backup restore s3:purros-20260927-020012-scheduled.purros-backup --replace
docker compose start api
docker compose exec api purros doctor

What restore does:

  1. It verifies the whole backup (every checksum) before changing anything.
  2. It refuses a backup from a newer PurrOS: update PurrOS first.
  3. It warns if the backup was made with a different PURROS_SECRET, or if a PurrOS server or worker is still connected.
  4. When the database already has data, it needs --replace and asks you to type the company name. It then saves a safety backup of the current data first (skip with --no-safety-backup).
  5. It rebuilds the schema at the backup's version, and loads every table with all references re-checked.
  6. It migrates to the current version. This means a backup from an older PurrOS can be restored into a newer one.

Backups that include uploaded files put them back into the configured storage. Otherwise, restore the state volume or bucket to the same point in time.

Test a restore on a spare machine at least every few months: purros backup restore into an empty database, then purros doctor.

Upgrading

Upgrades are done by hand. PurrOS doesn't download or install new versions itself. It uses semantic versioning:

  • Patch releases (1.2.3 → 1.2.4): fixes only.
  • Minor releases (1.2 → 1.3): new features, no breaking API changes. Migrations run with the app online.
  • Major releases (1.x → 2.0): may change the API or need downtime. Read the upgrade notes first.

Steps:

# 1. Back up, and copy the backup off the server
docker compose exec api purros backup create
# 2. Read the release notes for anything marked "Action required"
# 3. Get the new version and start it (migrations run automatically on start)
docker compose up -d --build
# 4. Check
docker compose exec api purros migrate status
docker compose exec api purros doctor

Without Docker: take a backup, replace the purros binary, run purros migrate, restart the service, then run purros doctor.

  • Migrations are forward-only and designed to run while PurrOS is live. Any exception is flagged in the release notes.
  • Don't skip major versions. Upgrade 1.x → 2.x → 3.x in order.
  • Downgrading means going back to the previous version and restoring the backup taken before the upgrade.

Before an upgrade, check

  • Backup finished and copied off the server
  • Release notes read
  • Integrations don't use anything listed as deprecated or removed
  • A quiet time chosen (few users, and after the nightly jobs)

On this page