Skip to content

Backup & restore

The complete state of a Schleuse instance is two things: the database and the /app/data directory (the app_data volume). Back up both and you can restore the IdP anywhere.

What to back up

WhatWhereWhy it matters
DatabasePostgres, or schleuse.db inside /app/data (SQLite)users, credentials, OIDC tokens, audit log, email codes
data/jwt-secret.txt/app/dataHMAC secret for session cookies
data/jwt-private-key.pem/app/dataRSA key for OIDC ID-token / JWKS signing

Don't lose the keys

Key resolution is env var > persisted file > auto-generate. If a restored instance is missing jwt-private-key.pem, it silently generates a new one — which changes the JWKS kid (all issued ID/JWT-access tokens fail verification) and changes every confidential client secret (they're derived as sha256(key + clientId)). A missing jwt-secret.txt logs everyone out. Always back up /app/data, or set NUXT_JWT_SECRET / NUXT_JWT_PRIVATE_KEY explicitly so the values are reproducible from your secret store.

Your config is env-only (.env / compose), so version-control or secret-store those separately — they aren't in /app/data.

Backup

Database — Postgres

bash
# dump (compressed)
docker exec schleuse-db pg_dump -U schleuse_user schleuse | gzip > schleuse-$(date +%F).sql.gz

A nightly cron is enough for most deployments:

cron
0 3 * * *  docker exec schleuse-db pg_dump -U schleuse_user schleuse | gzip > /backups/schleuse-$(date +\%F).sql.gz

The file is live (WAL mode), so don't cp it under load and don't tar the whole volume — that captures stale -wal/-shm files that corrupt the snapshot. Use SQLite's online backup to a single clean file. The app image is a minimal Node image (no sqlite3 CLI), so run it from a throwaway alpine container on the same app_data volume, writing straight to the host:

bash
docker run --rm -v app_data:/data -v "$PWD:/out" alpine \
  sh -c "apk add -q sqlite && sqlite3 /data/schleuse.db \".backup '/out/schleuse-$(date +%F).db'\""

Simplest alternative — stop the app and copy the file directly:

bash
docker compose stop app
docker run --rm -v app_data:/data -v "$PWD:/out" alpine cp /data/schleuse.db /out/schleuse-$(date +%F).db
docker compose start app

The keys (/app/data)

The signing keys are tiny static files — back them up separately (for Postgres the volume holds only these):

bash
docker run --rm -v app_data:/data -v "$PWD:/out" alpine \
  tar czf /out/keys-$(date +%F).tgz -C /data jwt-secret.txt jwt-private-key.pem

Restore

Restore onto a fresh host (or after data loss). Stop the app first so nothing writes mid-restore.

  1. Stop the app: docker compose stop app (or down if recreating).
  2. Restore the keys into the volume:
    bash
    docker run --rm -v app_data:/data -v "$PWD:/in" alpine \
      tar xzf /in/keys-YYYY-MM-DD.tgz -C /data
  3. Restore the database:
    • SQLite — copy the clean snapshot in as schleuse.db and make sure no stale WAL files come with it:
      bash
      docker run --rm -v app_data:/data -v "$PWD:/in" alpine \
        sh -c "rm -f /data/schleuse.db /data/schleuse.db-wal /data/schleuse.db-shm; cp /in/schleuse-YYYY-MM-DD.db /data/schleuse.db"
    • Postgres:
      bash
      gunzip < schleuse-YYYY-MM-DD.sql.gz | docker exec -i schleuse-db psql -U schleuse_user schleuse
  4. Fix ownership — the alpine helper writes files as root, but the app runs as the non-root app user (UID 100 in the published image) and must own its data or it can't write the database ("readonly database"):
    bash
    docker run --rm -v app_data:/data alpine chown -R 100:101 /data
  5. Start the app: docker compose up -d app. Migrations run automatically on start.
  6. Verify (see below).

Migrations are forward-only

Restoring an older DB into a newer image is fine — migrations auto-apply on startup. Restoring a newer DB into an older image is not supported, so restore into the same or a newer version.

Verify a restore

bash
# 1. App healthy + expected version
curl -s https://auth.example.com/api/ready        # {"status":"ready","database":"connected",...}

# 2. Signing key preserved — the kid must match the pre-restore value
curl -s https://auth.example.com/.well-known/jwks.json | jq '.keys[0].kid'

Then do a real login and one OIDC authorization flow. A matching kid is the proof your RSA key survived — if it changed, you restored without the keys and every relying party will reject tokens until you fix it.

Test your restore

A backup you've never restored is a hope, not a backup. On a schedule, restore the latest archive into a throwaway staging instance and confirm: /api/ready, a login, and an OIDC flow. Catch a broken backup before you need it.

A small but polished sidecar identity provider.