Docker Compose
TL;DR — Two canonical compose files: SQLite (single instance,
<1000clients) and PostgreSQL (HA-ready, needsauthserver migrateon first start). Both use the sameauthplane/authserver:latestimage and expose OAuth on:9000while keeping the admin port on127.0.0.1:9001only. Add Caddy for automatic Let’s Encrypt TLS.
SQLite (single instance)
Best for small deployments: single server, <1000 clients, no HA requirement.
docker-compose.yml:
services:
authserver:
image: authplane/authserver:latest
ports:
- "9000:9000"
- "127.0.0.1:9001:9001" # Admin API on loopback only
environment:
AUTHPLANE_SERVER_ISSUER: https://auth.example.com
AUTHPLANE_STORAGE_SQLITE_PATH: /data/authserver.db
AUTHPLANE_SIGNING_KEY_PATH: /data/keys
AUTHPLANE_SESSION_SECRET: ${SESSION_SECRET}
AUTHPLANE_SESSION_SECURE: "true"
AUTHPLANE_ADMIN_API_KEY: ${ADMIN_API_KEY}
AUTHPLANE_RESOURCE_URI: http://mcp-server:3000/mcp
AUTHPLANE_RESOURCE_SCOPES: tools/echo
volumes:
- authserver-data:/data
restart: unless-stopped
healthcheck:
test: ["CMD", "/authserver", "version"]
interval: 10s
timeout: 5s
retries: 3
volumes:
authserver-data:
.env:
SESSION_SECRET=generate-a-32-byte-random-string
ADMIN_API_KEY=generate-a-secure-api-key
Generate secrets:
openssl rand -base64 32 # session secret
openssl rand -hex 24 # admin API key
Start:
docker compose up -d
PostgreSQL (multi-instance ready)
For HA or when you want a shared database. Adds a Postgres 18-alpine service, wires DSN via env var, and includes a healthcheck-gated depends_on so authserver waits for Postgres to be ready.
docker-compose.yml:
services:
postgres:
image: postgres:18-alpine
environment:
POSTGRES_DB: authserver
POSTGRES_USER: authserver
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- pg-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U authserver"]
interval: 5s
timeout: 3s
retries: 5
authserver:
image: authplane/authserver:latest
ports:
- "9000:9000"
- "127.0.0.1:9001:9001"
environment:
AUTHPLANE_SERVER_ISSUER: https://auth.example.com
AUTHPLANE_STORAGE_DRIVER: postgres
AUTHPLANE_STORAGE_POSTGRES_DSN: postgres://authserver:${POSTGRES_PASSWORD}@postgres:5432/authserver?sslmode=disable
AUTHPLANE_SIGNING_KEY_PATH: /data/keys
AUTHPLANE_SESSION_SECRET: ${SESSION_SECRET}
AUTHPLANE_SESSION_SECURE: "true"
AUTHPLANE_ADMIN_API_KEY: ${ADMIN_API_KEY}
AUTHPLANE_RESOURCE_URI: http://mcp-server:3000/mcp
AUTHPLANE_RESOURCE_SCOPES: tools/echo
volumes:
- authserver-keys:/data/keys
depends_on:
postgres:
condition: service_healthy
restart: unless-stopped
healthcheck:
test: ["CMD", "/authserver", "version"]
interval: 10s
timeout: 5s
retries: 3
volumes:
pg-data:
authserver-keys:
Run migrations before first start:
docker compose run --rm authserver migrate
Then start:
docker compose up -d
The sslmode=disable in the DSN is fine when authserver and postgres share a Docker network. If you point at an external Postgres, set sslmode=require (or verify-full with a sslrootcert= path).
With a reverse proxy (Caddy) — automatic TLS
Caddy handles Let’s Encrypt certificates automatically. Add it in front of authserver:
docker-compose.yml:
services:
caddy:
image: caddy:2-alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy-data:/data
depends_on:
- authserver
authserver:
image: authplane/authserver:latest
environment:
AUTHPLANE_SERVER_ISSUER: https://auth.example.com
AUTHPLANE_STORAGE_SQLITE_PATH: /data/authserver.db
AUTHPLANE_SIGNING_KEY_PATH: /data/keys
AUTHPLANE_SESSION_SECRET: ${SESSION_SECRET}
AUTHPLANE_SESSION_SECURE: "true"
AUTHPLANE_ADMIN_API_KEY: ${ADMIN_API_KEY}
volumes:
- authserver-data:/data
volumes:
caddy-data:
authserver-data:
Caddyfile:
auth.example.com {
reverse_proxy authserver:9000
}
The admin port (:9001) is intentionally NOT exposed through the reverse proxy. Access the Admin UI at http://127.0.0.1:9001/admin/ui/ from the Docker host — SSH tunnel, VPN, or bastion for remote access.
Backup
SQLite
The database file and signing keys live in the authserver-data volume:
# Stop authserver for consistent backup
docker compose stop authserver
# Copy the volume data
docker run --rm -v authserver-data:/data -v $(pwd)/backup:/backup \
alpine tar czf /backup/authserver-$(date +%Y%m%d).tar.gz -C /data .
# Restart
docker compose start authserver
For live backups, run SQLite’s .backup from the host against the mounted volume — the AuthPlane image is distroless and has no shell or sqlite3 binary, so docker compose exec … sqlite3 … will fail. WAL mode makes a host-side .backup safe without stopping the container. Details in Backup, upgrade, purge.
PostgreSQL
docker compose exec postgres pg_dump -U authserver authserver > backup.sql
Or use continuous archiving (WAL-E, pgbackrest) for point-in-time recovery.
Upgrading
docker compose pull authserver
docker compose run --rm authserver migrate # Run any new migrations
docker compose up -d
Migrations are forward-only and idempotent — running them on an already-migrated database is a no-op. Read the release notes before major version bumps.
Operational features
Runtime features that apply to Docker Compose as much as standalone:
-
SIGHUP key reload — hot-reload signing keys without a restart:
docker kill -s HUP $(docker compose ps -q authserver)Or use
POST /admin/keys/rotatevia the Admin API. -
PostgreSQL LISTEN/NOTIFY — when
storage.driver: postgres, config changes (new resources, allowlist updates) propagate to all instances in milliseconds. SQLite deployments poll caches every 30 seconds instead. -
Zero-downtime key rotation — new keys are added; old keys stay in JWKS for verification until they expire. See Security: Key management.
-
authserver purge— scheduled cleanup for expired tokens, DPoP nonces, and assertion JTIs. Not automatic; schedule via a compose sidecar or host crontab. See Backup, upgrade, purge.
SQLite cache propagation window
With AUTHPLANE_STORAGE_DRIVER=sqlite, resource-server / allowlist / broker-provider changes made via the Admin API take up to 30 seconds to become visible to /.well-known/oauth-authorization-server, scope validation, and the token endpoint — the in-memory caches refresh on a 30 s tick and SQLite has no LISTEN/NOTIFY. PostgreSQL deployments propagate in milliseconds via PG NOTIFY. Rarely matters outside initial bring-up.
Related
- Operate overview — the mode picker
- Standalone binary — the same on Linux without Docker
- Kubernetes (Helm) — the same at HA scale
- Vault Transit — HSM-grade signing in front of any mode
- Backup, upgrade, purge — data lifecycle
- Configuration reference — every env var