Docker Compose

TL;DR — Two canonical compose files: SQLite (single instance, <1000 clients) and PostgreSQL (HA-ready, needs authserver migrate on first start). Both use the same authplane/authserver:latest image and expose OAuth on :9000 while keeping the admin port on 127.0.0.1:9001 only. 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/rotate via 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.