Backup & Migrations
pg_dump, make db-migrate, and restore procedures.
Backup & Migrations
What to back up
Three volumes hold durable state:
| Volume | Contents | Backup frequency |
|---|---|---|
postgres_data | All Postgres data (identities, sessions, Keto tuples, Hydra clients) | Every 6h + WAL archive |
minio_data | Logos, favicons, audit exports, data exports | Nightly |
traefik-letsencrypt | Issued TLS certs | Weekly |
Nothing else carries irrecoverable state: Valkey is a cache, NATS is a queue (messages are replayed from audit), Kratos courier retries from Postgres.
Postgres backup
Option A -- pg_dump (simplest)
Run nightly against the PgBouncer endpoint (bypasses connection limits on the primary):
docker compose -f docker-compose.traefik.yml exec -T postgres \
pg_dump -U avnology -d avnology --format=custom \
| gzip > avnology-$(date +%Y%m%d).dump.gzRetain 30 days rolling. For PITR you need WAL archiving -- see Option B.
Option B -- pgBackRest with WAL archiving (recommended for prod)
The shipped deploy/docker/postgres/postgresql.conf enables WAL archiving to /var/lib/postgresql/archive/. Pair with pgbackrest running as a sidecar:
# add to docker-compose.traefik.yml
pgbackrest:
image: pgbackrest/pgbackrest:2.53
volumes:
- postgres_data:/var/lib/postgresql/data:ro
- postgres_archive:/var/lib/postgresql/archive
- ./deploy/docker/pgbackrest/pgbackrest.conf:/etc/pgbackrest.conf:ro
- backup_storage:/var/lib/pgbackrestSchedule pgbackrest --stanza=avnology --type=full backup weekly and --type=incr every 6h via cron or a systemd timer.
MinIO backup
The mc mirror sidecar replicates the minio_data volume to an external S3 bucket:
mc mirror --overwrite --remove \
avnology-local/avnology-branding s3://offsite-bucket/avnology-brandingRun nightly via a cron container or your orchestrator.
Running migrations
Database schema migrations ship as SQL files in deploy/docker/migrations/. The avnology-migrate sidecar runs on every stack boot, applying any new migrations idempotently.
To run migrations without restarting the stack:
make db-migrate
# or equivalently:
docker compose -f docker-compose.traefik.yml run --rm avnology-migrateRollback of a single migration:
docker compose -f docker-compose.traefik.yml run --rm \
-e MIGRATE_COMMAND=down \
avnology-migrateMigrations are append-only -- never edit a migration that has shipped to production.
Restore procedure
Scenario: catastrophic loss, full restore
- Stop the stack:
docker compose down. - Restore the Postgres volume from your backup.
- Restore the MinIO volume.
docker compose -f docker-compose.traefik.yml up -d.- Verify with
make healthcheck.
Scenario: Postgres-only corruption, point-in-time recovery
If you ran pgBackRest (Option B):
docker compose stop postgres
docker volume rm avnology_postgres_data
docker run --rm \
-v avnology_postgres_data:/var/lib/postgresql/data \
-v backup_storage:/var/lib/pgbackrest \
pgbackrest/pgbackrest:2.53 \
--stanza=avnology --type=time --target="2026-04-17 12:00:00" restore
docker compose up -d postgresScenario: accidental delete of one organization
Use the Governance audit trail to enumerate the deleted records, then replay writes from the nightly dump into a staging DB and export just the affected rows. There is no "single org restore" RPC today.
Migrating managed -> self-hosted
- In managed, request a data export via
POST /v1/admin/exports. You receive a signed URL to a.dumpfile. - Provision your self-hosted stack with blank Postgres.
- Restore the dump:
docker compose cp exported.dump postgres:/tmp/
docker compose exec postgres \
pg_restore -U avnology -d avnology --clean --if-exists /tmp/exported.dump- Run
make db-migrateto bring schema to latest. - DNS swap: repoint
id.your-company.comat your self-hosted Traefik.