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, roles, app metadata, 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.

The database does not grow without bound. The OIDC token table holds one row per access token, refresh token, authorization code, session and grant, and used to keep expired rows forever — which mattered most for a service client, since client_credentials mints a token per call and never reuses one. Expired rows are now deleted hourly, so dump size tracks live state rather than lifetime issuance. Nothing to configure, and nothing you can lose by restoring an older dump: an expired token is already unusable.

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",...}
# A restored database may still need migrations — a brief 503 with
# "database":"migrating" first is expected. Wait for "ready".

# 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. · llms.txt