Skip to content

Docker Deployment

For local development, run the published image with SQLite. Generate an admin API key, write a minimal config, and start the server:

Terminal window
docker pull authplane/authserver:latest
export AUTHPLANE_ADMIN_API_KEY="$(openssl rand -hex 32)"
config.yaml
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 path
token_exchange:
enabled: true # RFC 8693 — agent delegation and Token Vault upstreams
max_chain_depth: 5
token_expiry: 1h
Terminal window
docker 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.issuer
curl -fsS http://localhost:9000/.well-known/oauth-authorization-server | jq -r .issuer
curl -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.

Terminal window
git clone https://github.com/authplane/authserver.git && cd authserver
export 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 -d

The 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 the pgdata volume and a pg_isready health check gating the auth server’s start. The password is baked in as authserver — change both POSTGRES_PASSWORD and the DSN for any non-local use.
  • authserver — public OAuth surface on :9000, admin API + UI + /metrics on :9001. Signing keys persist in the keydata volume. The reference file builds from the checkout (build:); swap in image: authplane/authserver:latest to 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 the purge profile. It is not automatic — run on demand with docker compose --profile purge run --rm authserver-purge, or schedule via host cron: 0 * * * * docker compose -f deploy/docker-compose.yml run --rm authserver-purge.

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.

  • 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 the keydata volume. See backup & purge.

Next: Kubernetes deployment · Configuration · Observability