Docker Deployment
Single-container quickstart
Section titled “Single-container quickstart”For local development, run the published image with SQLite. Generate an admin API key, write a minimal config, and start the server:
docker pull authplane/authserver:latestexport AUTHPLANE_ADMIN_API_KEY="$(openssl rand -hex 32)"server: issuer: "http://localhost:9000" address: ":9000"
storage: driver: sqlite sqlite: path: "/data/authserver.db" wal: true
signing: algorithm: ES256 key_path: "/data/keys"
dcr: mode: open
admin: enabled: true address: ":9001" api_key: "${AUTHPLANE_ADMIN_API_KEY}"
client_credentials: enabled: true # machine-to-machine — the simplest MCP-server pathtoken_exchange: enabled: true # RFC 8693 — agent delegation and Token Vault upstreams max_chain_depth: 5 token_expiry: 1hdocker run -d --name authserver \ -v $(pwd)/config.yaml:/config.yaml:ro \ -v authserver-data:/data \ -p 9000:9000 \ -p 9001:9001 \ authplane/authserver:latest serve --config /config.yaml
# Verify — issuer should match server.issuercurl -fsS http://localhost:9000/.well-known/oauth-authorization-server | jq -r .issuercurl -fsS http://localhost:9000/health | jq .Signing keys are generated under /data/keys on first boot; the SQLite database and keys persist in the authserver-data volume. SQLite is fine for a single instance — use the PostgreSQL stack below for anything production-shaped.
Production-shaped stack: PostgreSQL 18 + observability
Section titled “Production-shaped stack: PostgreSQL 18 + observability”The repository ships a full reference stack at deploy/docker-compose.yml: PostgreSQL 18, the auth server, a one-shot setup job, an on-demand purge job, and the Grafana LGTM observability stack pulled in via Compose’s include: directive.
git clone https://github.com/authplane/authserver.git && cd authserverexport AUTHPLANE_SESSION_SECRET="$(openssl rand -hex 32)"export AUTHPLANE_ADMIN_API_KEY="$(openssl rand -hex 32)"docker compose -f deploy/docker-compose.yml up --build -dThe compose file uses ${VAR:?...} syntax, so missing secrets fail fast with a readable error. Migrations run automatically on first boot — authserver serve migrates before binding the listener. Grafana comes up at http://localhost:3000 (admin/admin); Prometheus scrapes authserver:9001/metrics; logs and traces flow through Alloy at :4317 (OTLP gRPC).
The service shapes, faithfully from deploy/docker-compose.yml:
include: - path: observability/docker-compose.observability.yml
services: postgres: image: postgres:18-alpine container_name: postgres environment: POSTGRES_DB: authserver POSTGRES_USER: authserver POSTGRES_PASSWORD: authserver ports: - "5432:5432" volumes: # PG18's image stores data in a version subdir and rejects a mount at the # legacy /var/lib/postgresql/data; mount the parent instead. - pgdata:/var/lib/postgresql healthcheck: test: ["CMD-SHELL", "pg_isready -U authserver"] interval: 5s timeout: 5s retries: 5 restart: unless-stopped
authserver: build: context: .. dockerfile: build/Dockerfile container_name: authserver ports: - "9000:9000" - "9001:9001" environment: AUTHPLANE_SERVER_ISSUER: http://localhost:9000 AUTHPLANE_SESSION_SECRET: ${AUTHPLANE_SESSION_SECRET:?set AUTHPLANE_SESSION_SECRET (e.g. openssl rand -hex 32)} AUTHPLANE_SESSION_SECURE: "false" AUTHPLANE_SERVER_ALLOWED_ORIGINS: "*" AUTHPLANE_ADMIN_API_KEY: ${AUTHPLANE_ADMIN_API_KEY:?set AUTHPLANE_ADMIN_API_KEY (e.g. openssl rand -hex 32)} AUTHPLANE_STORAGE_DRIVER: postgres AUTHPLANE_STORAGE_POSTGRES_DSN: postgres://authserver:authserver@postgres:5432/authserver?sslmode=disable AUTHPLANE_SIGNING_KEY_PATH: /data/keys AUTHPLANE_LOG_FORMAT: json AUTHPLANE_LOG_LEVEL: debug AUTHPLANE_LOG_STDOUT: "true" AUTHPLANE_LOG_OTEL: "true" AUTHPLANE_LOG_OTEL_ENDPOINT: alloy:4317 AUTHPLANE_LOG_OTEL_INSECURE: "true" AUTHPLANE_TRACING_ENABLED: "true" AUTHPLANE_TRACING_ENDPOINT: alloy:4317 AUTHPLANE_TRACING_INSECURE: "true" AUTHPLANE_TRACING_SAMPLE_RATE: "1.0" AUTHPLANE_METRICS_PROVIDER: both AUTHPLANE_METRICS_OTEL_ENDPOINT: alloy:4317 AUTHPLANE_METRICS_INSECURE: "true" AUTHPLANE_CLIENT_CREDENTIALS_ENABLED: "true" AUTHPLANE_DPOP_ENABLED: "true" AUTHPLANE_DPOP_NONCE_TTL: "5m" AUTHPLANE_DPOP_PROOF_LIFETIME: "120s" AUTHPLANE_DPOP_PURGE_INTERVAL: "10m" AUTHPLANE_TOKEN_EXCHANGE_ENABLED: "true" AUTHPLANE_TOKEN_EXCHANGE_MAX_CHAIN_DEPTH: "5" volumes: - keydata:/data/keys healthcheck: test: ["CMD", "/authserver", "version"] interval: 5s timeout: 3s retries: 5 depends_on: postgres: condition: service_healthy alloy: condition: service_started restart: unless-stopped
setup: image: curlimages/curl:8.19.0 container_name: authserver-setup volumes: - ./setup.sh:/setup.sh:ro entrypoint: ["sh", "/setup.sh"] environment: AUTHPLANE_ADMIN_API_KEY: ${AUTHPLANE_ADMIN_API_KEY} depends_on: authserver: condition: service_healthy restart: "no"
authserver-purge: build: context: .. dockerfile: build/Dockerfile container_name: authserver-purge command: ["purge"] environment: AUTHPLANE_STORAGE_DRIVER: postgres AUTHPLANE_STORAGE_POSTGRES_DSN: postgres://authserver:authserver@postgres:5432/authserver?sslmode=disable AUTHPLANE_LOG_FORMAT: json AUTHPLANE_LOG_STDOUT: "true" depends_on: postgres: condition: service_healthy profiles: ["purge"] restart: "no"
volumes: pgdata: keydata:What each piece does:
postgres— PostgreSQL 18 with data in thepgdatavolume and apg_isreadyhealth check gating the auth server’s start. The password is baked in asauthserver— change bothPOSTGRES_PASSWORDand the DSN for any non-local use.authserver— public OAuth surface on:9000, admin API + UI +/metricson:9001. Signing keys persist in thekeydatavolume. The reference file builds from the checkout (build:); swap inimage: authplane/authserver:latestto run the published image instead.setup— one-shot job that waits for the auth server to be healthy, then creates a demo user (demo@example.com/demo-password).authserver-purge— expired-row cleanup behind thepurgeprofile. It is not automatic — run on demand withdocker compose --profile purge run --rm authserver-purge, or schedule via host cron:0 * * * * docker compose -f deploy/docker-compose.yml run --rm authserver-purge.
TLS with a reverse proxy
Section titled “TLS with a reverse proxy”Terminate TLS in front of :9000 and never expose :9001 publicly. Drop a caddy:2-alpine service alongside the stack (ports 80/443, depends_on: [authserver]) with a one-line Caddyfile:
auth.example.com { reverse_proxy authserver:9000}Once TLS is live, set AUTHPLANE_SERVER_ISSUER to https://auth.example.com and AUTHPLANE_SESSION_SECURE to true.
Runbook
Section titled “Runbook”- Upgrade:
docker compose pull authserver && docker compose up -d. Migrations are forward-only and idempotent. - Rotate signing keys:
docker compose exec authserver authserver admin key rotate— zero-downtime; the previous key remains in JWKS until expiry. - Backup: dump Postgres (
pg_dump) and back up thekeydatavolume. See backup & purge.
Next: Kubernetes deployment · Configuration · Observability