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
| What | Where | Why it matters |
|---|---|---|
| Database | Postgres, or schleuse.db inside /app/data (SQLite) | users, credentials, OIDC tokens, audit log, email codes |
data/jwt-secret.txt | /app/data | HMAC secret for session cookies |
data/jwt-private-key.pem | /app/data | RSA 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
# dump (compressed)
docker exec schleuse-db pg_dump -U schleuse_user schleuse | gzip > schleuse-$(date +%F).sql.gzA nightly cron is enough for most deployments:
0 3 * * * docker exec schleuse-db pg_dump -U schleuse_user schleuse | gzip > /backups/schleuse-$(date +\%F).sql.gzThe 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:
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:
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 appThe keys (/app/data)
The signing keys are tiny static files — back them up separately (for Postgres the volume holds only these):
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.pemRestore
Restore onto a fresh host (or after data loss). Stop the app first so nothing writes mid-restore.
- Stop the app:
docker compose stop app(ordownif recreating). - 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 - Restore the database:
- SQLite — copy the clean snapshot in as
schleuse.dband make sure no stale WAL files come with it:bashdocker 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
- SQLite — copy the clean snapshot in as
- Fix ownership — the
alpinehelper writes files asroot, but the app runs as the non-rootappuser (UID 100 in the published image) and must own its data or it can't write the database ("readonly database"):bashdocker run --rm -v app_data:/data alpine chown -R 100:101 /data - Start the app:
docker compose up -d app. Migrations run automatically on start. - 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
# 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.
Related
- Deployment → Volumes and Secret rotation — the volume layout and per-secret rotation steps.
- Security → Incident response — what to do when a secret or key is compromised (rather than lost).