Backup, upgrade, purge
TL;DR — Three operational chores. Backup the storage driver (SQLite file /
pg_dump) plus the signing keys directory. Upgrade by pulling the new binary or image and runningauthserver migrate; migrations are forward-only and idempotent. Purge is not automatic inserve— scheduleauthserver purgeexternally (systemd timer, Docker sidecar, or k8s CronJob) or expired-data tables grow unbounded.
Backup
SQLite
The database file and signing keys must be backed up together.
Cold backup (safe, requires stopping the server):
# Standalone
systemctl stop authserver
cp /var/lib/authserver/authserver.db /backup/authserver-$(date +%Y%m%d).db
cp -r /var/lib/authserver/keys /backup/keys-$(date +%Y%m%d)
systemctl start authserver
# Docker Compose
docker compose stop authserver
docker run --rm -v authserver-data:/data -v $(pwd)/backup:/backup \
alpine tar czf /backup/authserver-$(date +%Y%m%d).tar.gz -C /data .
docker compose start authserver
Live backup (no downtime, WAL mode required):
sqlite3 /var/lib/authserver/authserver.db \
".backup /backup/authserver-$(date +%Y%m%d).db"
SQLite’s .backup command holds a shared lock briefly and produces a consistent snapshot while writes continue. Enable WAL mode (default) for concurrent-read semantics: storage.sqlite.wal: true.
PostgreSQL
# One-shot
docker compose exec postgres pg_dump -U authserver authserver > backup.sql
# Or from a standalone Postgres host
pg_dump -h db.internal -U authserver authserver > backup.sql
For production, use continuous archiving (WAL-E, pgBackRest, barman) for point-in-time recovery. AuthPlane’s schema is standard PostgreSQL — any Postgres-aware backup tool works.
Signing keys
Always back up the signing keys directory. If you lose them, every outstanding JWT becomes unverifiable — clients will hit 401 invalid_token until they re-authenticate.
- Keyfile store — back up
/var/lib/authserver/keys/(standalone) or theauthserver-datavolume (Docker) or the persistent volume (Kubernetes). postgres_keystore — keys are in the Postgres backup already; nothing extra to back up.vault_transitstore — Vault is the source of truth. Follow your Vault backup procedure; AuthPlane never has the private key on disk.
Upgrade
AuthPlane follows semantic versioning. Same-major-line upgrades are drop-in; a major bump signals a breaking change and comes with a migration note in the release.
Standalone binary:
systemctl stop authserver
cp /usr/local/bin/authserver /usr/local/bin/authserver.bak
curl -L https://github.com/authplane/authserver/releases/latest/download/authserver-linux-amd64 \
-o /usr/local/bin/authserver
chmod +x /usr/local/bin/authserver
sudo -u authserver authserver migrate --config /etc/authserver/config.yaml
systemctl start authserver
Docker Compose:
docker compose pull authserver
docker compose run --rm authserver migrate # apply any new migrations
docker compose up -d
Kubernetes (Helm):
helm upgrade authplane oci://ghcr.io/authplane/charts/authplane \
--version <new-version> \
-f values-production.yaml
The Helm chart runs migrations automatically on pod boot — no separate migration Job needed. The init container waits for Postgres connectivity; the main process embeds all migration SQL via go:embed and applies pending migrations before serving traffic.
Migrations are forward-only and idempotent
Running authserver migrate on an already-migrated database is a no-op. There is no rollback — restore from backup if you need to revert.
Read the release notes before major-version bumps. Breaking changes are called out in CHANGELOG.md with concrete migration steps.
Scheduled purge
serve does not run purge goroutines. With dpop.enabled, client_credentials.enabled, or xaa.enabled set to true, you MUST schedule authserver purge externally or the expired-data tables grow unbounded.
What authserver purge deletes
One pass deletes expired rows from every purgeable table:
Default is all targets; --only=target1,target2 runs a subset. --timeout defaults to 10m; pass --timeout=0 to disable the internal deadline. Aborts on SIGINT/SIGTERM.
Consent grants and other never-expire rows are not touched — they age out via revocation, not expiration.
Recommended schedule
A single daily run is enough for most deployments. Operators who issue many short-lived tokens (DPoP proofs, machine tokens) may prefer hourly to keep tables small. The purge is a set of lightweight DELETE WHERE expires_at < now() queries — it does not block OAuth traffic.
Scheduled purge — systemd timer
For standalone binary deployments.
/etc/systemd/system/authserver-purge.service:
[Unit]
Description=AuthPlane expired-data purge
After=network-online.target
[Service]
Type=oneshot
User=authserver
Group=authserver
ExecStart=/usr/local/bin/authserver purge --config /etc/authserver/config.yaml
/etc/systemd/system/authserver-purge.timer:
[Unit]
Description=Run authserver purge hourly
[Timer]
OnCalendar=hourly
RandomizedDelaySec=5m
Persistent=true
[Install]
WantedBy=timers.target
Enable:
systemctl daemon-reload
systemctl enable --now authserver-purge.timer
systemctl list-timers authserver-purge.timer
Inspect recent runs:
journalctl -u authserver-purge.service --since '1 day ago'
Scheduled purge — Docker Compose sidecar
Add a second service alongside authserver that reuses the same image and env — kept out of docker compose up by default via profiles: ["purge"]:
services:
authserver:
image: authplane/authserver:latest
# ... your normal config ...
authserver-purge:
image: authplane/authserver:latest
command: ["purge"]
environment:
# Must match authserver's storage config — purge hits the same DB.
AUTHPLANE_STORAGE_DRIVER: postgres
AUTHPLANE_STORAGE_POSTGRES_DSN: postgres://authserver:${POSTGRES_PASSWORD}@postgres:5432/authserver?sslmode=disable
depends_on:
postgres:
condition: service_healthy
restart: "no"
profiles: ["purge"]
Trigger from the host — cron or on demand:
# On-demand
docker compose run --rm authserver-purge
# Hourly via host crontab (crontab -e)
0 * * * * cd /opt/authserver && docker compose run --rm authserver-purge >> /var/log/authserver-purge.log 2>&1
Prefer host-level scheduling over stuffing cron inside the container — keeps the image minimal and failures visible to your standard monitoring.
Scheduled purge — Kubernetes CronJob
apiVersion: batch/v1
kind: CronJob
metadata:
name: authserver-purge
namespace: authserver
spec:
schedule: "0 * * * *" # hourly
concurrencyPolicy: Forbid # don't overlap runs
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 1
template:
spec:
restartPolicy: OnFailure
containers:
- name: purge
image: authplane/authserver:latest
args: ["purge"]
envFrom:
- configMapRef:
name: authserver-config
- secretRef:
name: authserver-secrets
resources:
requests: { cpu: 50m, memory: 64Mi }
limits: { cpu: 500m, memory: 256Mi }
The CronJob must share the same storage config (AUTHPLANE_STORAGE_* env) as the serve Deployment — it runs purge against the same database.
For “rows purged over time” charts, enable metrics on the CronJob pod the same way as serve (e.g. AUTHPLANE_METRICS_PROVIDER=otel with the OTLP endpoint).
Selective purge
Run just the high-churn tables hourly and everything else daily:
# Hourly
authserver purge --only=dpop-nonces,assertion-jti,jti --config /etc/authserver/config.yaml
# Daily
authserver purge --config /etc/authserver/config.yaml
--only accepts comma-separated target names from the table above.
Exit codes and alerting
authserver purge exits non-zero if any target fails or the context is canceled. Individual failures log at ERROR with a table=<target> attribute; the command continues with remaining targets and fails at the end. Wire the job’s exit status into your alerting:
- systemd —
OnFailure=in the service unit - Kubernetes — the CronJob’s
Jobfailure events; alert onkube_job_status_failed > 0 - Docker cron wrapper — grep the log for
ERRORand page on non-zero exit
Verifying a scheduled purge
Run manually once after setup:
authserver purge --timeout=5m
You should see INFO purge completed for each target.
What’s NOT part of these chores
- Rate-limiter cache — cleaned in-process every 5 minutes; not persisted, no scheduling needed.
- JWKS cache — invalidated by
SIGHUPorPOST /admin/keys/rotate; TTL-less otherwise. - In-memory registry caches — none. The unified resource model reads from the DB on every request.
Related
- Operate overview — mode picker
- Standalone binary — systemd timer walkthrough
- Docker Compose — backup examples end to end
- Kubernetes (Helm) — Helm chart handles migrations automatically
- Security: Key management — signing key rotation policy
- Configuration reference — storage and signing knobs