Avnology ID
Self-Hosting

Backup & Migrations

pg_dump, make db-migrate, and restore procedures.

Backup & Migrations

What to back up

Three volumes hold durable state:

VolumeContentsBackup frequency
postgres_dataAll Postgres data (identities, sessions, Keto tuples, Hydra clients)Every 6h + WAL archive
minio_dataLogos, favicons, audit exports, data exportsNightly
traefik-letsencryptIssued TLS certsWeekly

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.gz

Retain 30 days rolling. For PITR you need WAL archiving -- see Option B.

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/pgbackrest

Schedule 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-branding

Run 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-migrate

Rollback of a single migration:

docker compose -f docker-compose.traefik.yml run --rm \
  -e MIGRATE_COMMAND=down \
  avnology-migrate

Migrations are append-only -- never edit a migration that has shipped to production.

Restore procedure

Scenario: catastrophic loss, full restore

  1. Stop the stack: docker compose down.
  2. Restore the Postgres volume from your backup.
  3. Restore the MinIO volume.
  4. docker compose -f docker-compose.traefik.yml up -d.
  5. 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 postgres

Scenario: 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

  1. In managed, request a data export via POST /v1/admin/exports. You receive a signed URL to a .dump file.
  2. Provision your self-hosted stack with blank Postgres.
  3. 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
  1. Run make db-migrate to bring schema to latest.
  2. DNS swap: repoint id.your-company.com at your self-hosted Traefik.