Blog · Guides

Keycloak with Docker Compose: from start-dev to production

A working Docker Compose setup for Keycloak and PostgreSQL: the environment variables, health checks, reverse proxy settings, realm import and the mistakes that break production.

Casdoor Team · November 8, 2026 · 4 min read

This guide is written by the team that builds Casdoor and describes the standards as published (listed under Sources), with notes on how Casdoor implements them as of November 8, 2026. Spotted a mistake? Tell us.

Keycloak is easy to start and easy to misconfigure. A docker run with start-dev is fine for a laptop, but production needs a real database, a stable hostname, TLS termination and health checks. This guide is the Compose setup we would put in front of a small production deployment, with the reasons for each setting.

Short answer: use the PostgreSQL-backed start command, pin the image version, set KC_HOSTNAME, enable health endpoints, and put Keycloak behind a reverse proxy that terminates TLS. Keep start-dev for development only.

Development vs production

Keycloak has two start commands:

Command What it does Use it for
start-dev HTTP enabled, strict hostname off, local cache Local development and testing
start Production defaults, strict hostname, requires configuration Anything other people use

start-dev uses an embedded H2 database and local caching. It is not a production setup: data is not durable in a way you should rely on, and clustering is not configured.

The Compose file

services:
  postgres:
    image: postgres:17
    environment:
      POSTGRES_DB: keycloak
      POSTGRES_USER: keycloak
      POSTGRES_PASSWORD: change-me
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U keycloak -d keycloak"]
      interval: 10s
      timeout: 5s
      retries: 5

  keycloak:
    image: quay.io/keycloak/keycloak:26.8
    command: start
    environment:
      KC_DB: postgres
      KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak
      KC_DB_USERNAME: keycloak
      KC_DB_PASSWORD: change-me
      KC_HOSTNAME: login.example.com
      KC_HTTP_ENABLED: "true"
      KC_PROXY_HEADERS: xforwarded
      KC_HEALTH_ENABLED: "true"
      KC_METRICS_ENABLED: "true"
      KC_BOOTSTRAP_ADMIN_USERNAME: admin
      KC_BOOTSTRAP_ADMIN_PASSWORD: change-me-too
    ports:
      - "8080:8080"
      - "9000:9000"
    depends_on:
      postgres:
        condition: service_healthy

volumes:
  pgdata:

Pin the image tag. latest makes upgrades unrepeatable and can pull a new major version on a restart.

What each setting is for

  • KC_DB, KC_DB_URL, KC_DB_USERNAME, KC_DB_PASSWORD point Keycloak at PostgreSQL instead of the embedded database. Run PostgreSQL with a persistent volume and its own backup.
  • KC_HOSTNAME is the public URL users and applications see. If it is wrong, redirect URLs and discovery documents are wrong too.
  • KC_HTTP_ENABLED=true is correct only when a reverse proxy terminates TLS and forwards to Keycloak. If Keycloak handles TLS itself, configure the keystore instead.
  • KC_PROXY_HEADERS=xforwarded makes Keycloak read X-Forwarded-* headers so it sees the real scheme, host and client IP. Without it, redirects can loop or point at the wrong scheme.
  • KC_HEALTH_ENABLED and KC_METRICS_ENABLED expose health and metrics on the management port, 9000 by default. /health/ready tells a load balancer when the instance can serve traffic; /health/live tells it the process is alive.
  • KC_BOOTSTRAP_ADMIN_USERNAME and KC_BOOTSTRAP_ADMIN_PASSWORD create the first admin user. They only apply at first startup; change the password after the first login.

Behind a reverse proxy

Terminate TLS at Nginx, Caddy or your cloud load balancer, forward to port 8080, and keep the original headers:

location / {
    proxy_pass http://127.0.0.1:8080;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

Then set KC_HOSTNAME to the public hostname and keep KC_PROXY_HEADERS=xforwarded.

Importing a realm

Export a realm from a working instance and mount it into the container:

    volumes:
      - ./realm-export.json:/opt/keycloak/data/import/realm-export.json:ro
    command: start --import-realm

--import-realm imports files on startup when the realm does not already exist. It is useful for seeding clients and identity providers, not as a synchronization mechanism: changes made in the admin console are not written back to the file.

Upgrades and backups

  1. Back up the PostgreSQL database before every Keycloak upgrade.
  2. Read the upgrade notes for the target version; some releases change configuration or database schema.
  3. Run the new image against a copy of the database first.
  4. Keep the old image tag available for a rollback.

Keycloak does not need a separate migration container for the database: the server applies schema changes at startup. That makes the database backup the rollback plan.

Common mistakes

  • Running start-dev in production.
  • Using latest and restarting into an untested major version.
  • Setting KC_HOSTNAME to an internal name while users reach an external one.
  • Forgetting KC_PROXY_HEADERS, then debugging redirect loops.
  • Publishing PostgreSQL to the host instead of keeping it on the internal network.
  • Treating --import-realm as a two-way sync.

When Keycloak is more than you need

Keycloak is a full identity server: realms, clients, brokering, federation and an admin console. If you want the same protocols with a lighter operational footprint, Casdoor is an Apache-2.0 alternative that runs as a single service with organizations, SAML, OIDC, CAS, LDAP and RADIUS built in. Self-host it for free, or use a dedicated instance on Casdoor Cloud from $6.67/month billed yearly.

FAQ

What is the correct Docker image for Keycloak? The official image is quay.io/keycloak/keycloak. Pin a version tag such as 26.8 instead of latest.

Does Keycloak need PostgreSQL in production? The embedded H2 database is for development. Production deployments should use an external database such as PostgreSQL, with backups and monitoring.

What is the difference between start and start-dev? start-dev enables development defaults: HTTP, no strict hostname, local cache. start is the production command and expects explicit configuration.

How do I check if Keycloak is healthy? Enable KC_HEALTH_ENABLED and use /health/ready and /health/live on the management port, 9000 by default.

Can I run Keycloak and PostgreSQL in the same Compose file? Yes for small deployments. For production, consider a managed PostgreSQL service so backups, failover and upgrades are handled outside the Keycloak host.