Docker Compose Reference: Keycloak

Type: Software Reference Confidence: 0.93 Sources: 7 Verified: 2026-02-27 Freshness: 2026-02-27

TL;DR

Constraints

Quick Reference

Service Configuration

ServiceImagePortsVolumesKey Env
keycloakquay.io/keycloak/keycloak:26.08080:8080 (dev), 8443:8443 (prod)./realm-export:/opt/keycloak/data/importKC_DB=postgres
keycloak (health)(same)9000:9000(none)KC_HEALTH_ENABLED=true
postgrespostgres:16-alpine5432:5432pgdata:/var/lib/postgresql/dataPOSTGRES_DB=keycloak

Keycloak Environment Variables

VariablePurposeDev DefaultProd Recommendation
KC_BOOTSTRAP_ADMIN_USERNAMEInitial admin usernameadminSet via secret
KC_BOOTSTRAP_ADMIN_PASSWORDInitial admin passwordadminSet via secret
KC_DBDatabase vendordev-file (H2)postgres
KC_DB_URLJDBC connection URL(auto)jdbc:postgresql://postgres:5432/keycloak
KC_DB_USERNAMEDatabase user(auto)keycloak
KC_DB_PASSWORDDatabase password(auto)Set via secret
KC_HOSTNAMEPublic-facing hostname(none)https://auth.example.com
KC_PROXY_HEADERSProxy header forwarding(none)xforwarded
KC_HTTP_ENABLEDAllow plain HTTPtruetrue (behind proxy) or false
KC_HEALTH_ENABLEDHealth check endpointsfalsetrue
KC_METRICS_ENABLEDPrometheus metricsfalsetrue
KC_LOG_LEVELLogging verbosityinfoinfo or warn

Health & Readiness Endpoints (port 9000)

EndpointPurposeExpected Response
/healthOverall health{"status": "UP"}
/health/readyReadiness probe (K8s){"status": "UP"}
/health/liveLiveness probe (K8s){"status": "UP"}
/metricsPrometheus metricsPrometheus text format

Decision Tree

START: What deployment mode do you need?
├── Development / local testing?
│   ├── YES → Use start-dev with H2 (no database service needed)
│   └── NO ↓
├── Production single-node?
│   ├── YES → Use optimized build + PostgreSQL + reverse proxy
│   └── NO ↓
├── Production cluster (HA)?
│   ├── YES → Add KC_CACHE=ispn, open JGroups ports (7800), use shared DB
│   └── NO ↓
├── Need realm pre-configuration?
│   ├── YES → Mount realm JSON to /opt/keycloak/data/import + --import-realm
│   └── NO ↓
├── Behind reverse proxy (Nginx/Traefik/Caddy)?
│   ├── YES → Set KC_PROXY_HEADERS=xforwarded + KC_HTTP_ENABLED=true
│   └── NO ↓
└── DEFAULT → Production Compose with direct TLS termination on Keycloak

Step-by-Step Guide

1. Create development Docker Compose file

Start with a minimal development setup using start-dev mode. This uses Keycloak's built-in H2 database and disables HTTPS for local development. [src1]

# docker-compose.dev.yml -- Development only, DO NOT use in production
services:
  keycloak:
    image: quay.io/keycloak/keycloak:26.0
    command: start-dev
    environment:
      KC_BOOTSTRAP_ADMIN_USERNAME: admin
      KC_BOOTSTRAP_ADMIN_PASSWORD: admin
    ports:
      - "8080:8080"

Verify: curl -s http://localhost:8080/realms/master | jq .realm → expected: "master"

2. Create production Docker Compose with PostgreSQL

Production requires an external database, optimized builds, and proper hostname configuration. [src2]

# docker-compose.yml -- Production
services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: keycloak
      POSTGRES_USER: keycloak
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U keycloak"]
      interval: 10s
      timeout: 5s
      retries: 5
  keycloak:
    image: quay.io/keycloak/keycloak:26.0
    command: start --optimized
    environment:
      KC_BOOTSTRAP_ADMIN_USERNAME: ${KC_ADMIN_USER}
      KC_BOOTSTRAP_ADMIN_PASSWORD: ${KC_ADMIN_PASSWORD}
      KC_DB: postgres
      KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak
      KC_DB_USERNAME: keycloak
      KC_DB_PASSWORD: ${DB_PASSWORD}
      KC_HOSTNAME: https://auth.example.com
      KC_PROXY_HEADERS: xforwarded
      KC_HTTP_ENABLED: "true"
      KC_HEALTH_ENABLED: "true"
      KC_METRICS_ENABLED: "true"
    ports:
      - "8080:8080"
      - "9000:9000"
    depends_on:
      postgres:
        condition: service_healthy
    deploy:
      resources:
        limits:
          memory: 2G
        reservations:
          memory: 750M
volumes:
  pgdata:

Verify: curl -s http://localhost:9000/health/ready | jq .status → expected: "UP"

3. Configure reverse proxy (Nginx)

When running behind a TLS-terminating reverse proxy, set KC_PROXY_HEADERS=xforwarded and KC_HTTP_ENABLED=true. The proxy must forward X-Forwarded-* headers. [src5]

upstream keycloak {
    server keycloak:8080;
}
server {
    listen 443 ssl http2;
    server_name auth.example.com;
    ssl_certificate     /etc/ssl/certs/fullchain.pem;
    ssl_certificate_key /etc/ssl/private/privkey.pem;
    location / {
        proxy_pass http://keycloak;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Port $server_port;
        proxy_buffer_size 128k;
        proxy_buffers 4 256k;
        proxy_busy_buffers_size 256k;
    }
}

Verify: curl -sI https://auth.example.com/realms/master → expected: HTTP/2 200

4. Import a realm on startup

Export your realm configuration and mount it for automatic import on container startup. [src3]

# Add to keycloak service
services:
  keycloak:
    command: start --optimized --import-realm
    volumes:
      - ./realm-export.json:/opt/keycloak/data/import/realm-export.json:ro
# Export realm from running container
docker exec keycloak /opt/keycloak/bin/kc.sh export \
  --dir /opt/keycloak/data/export \
  --realm myrealm \
  --users realm_file

Verify: curl -s http://localhost:8080/realms/myrealm | jq .realm → expected: "myrealm"

5. Build an optimized production image

Create a multi-stage Dockerfile that runs the build step at image build time to reduce startup time from 30-60s to under 5s. [src1]

# Dockerfile.keycloak
FROM quay.io/keycloak/keycloak:26.0 AS builder
ENV KC_DB=postgres
ENV KC_HEALTH_ENABLED=true
ENV KC_METRICS_ENABLED=true
RUN /opt/keycloak/bin/kc.sh build

FROM quay.io/keycloak/keycloak:26.0
COPY --from=builder /opt/keycloak/ /opt/keycloak/
ENTRYPOINT ["/opt/keycloak/bin/kc.sh"]

Verify: Container starts in <5 seconds. Check: docker compose logs keycloak | grep "started in"

6. Add custom themes

Mount custom themes or bake them into the image for branded login/registration pages. [src4]

# Volume mount (development)
services:
  keycloak:
    volumes:
      - ./themes/my-theme:/opt/keycloak/themes/my-theme:ro

Verify: Admin Console → Realm Settings → Themes → select "my-theme" from dropdown

Code Examples

YAML: Complete production docker-compose.yml

Full script: production-docker-compose.yml (62 lines)

# Production Keycloak + PostgreSQL + Nginx
# Usage: DB_PASSWORD=secret KC_ADMIN_USER=admin KC_ADMIN_PASSWORD=secret docker compose up -d
services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: keycloak
      POSTGRES_USER: keycloak
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U keycloak"]
      interval: 10s
      retries: 5
# ... (see full script)

JSON: OIDC client realm export

Full script: realm-export-example.json (48 lines)

{
  "realm": "myapp",
  "enabled": true,
  "clients": [{
    "clientId": "myapp-frontend",
    "enabled": true,
    "publicClient": true,
    "redirectUris": ["http://localhost:3000/*"],
    "webOrigins": ["http://localhost:3000"],
    "protocol": "openid-connect"
  }]
}

Bash: Health check and readiness script

#!/usr/bin/env bash
# Input:  Keycloak health endpoint URL
# Output: Exit 0 if ready, exit 1 if not
KC_HEALTH_URL="${KC_HEALTH_URL:-http://localhost:9000/health/ready}"
MAX_RETRIES=30
RETRY_INTERVAL=2
for i in $(seq 1 "$MAX_RETRIES"); do
  status=$(curl -sf "$KC_HEALTH_URL" | jq -r '.status' 2>/dev/null)
  if [ "$status" = "UP" ]; then
    echo "Keycloak is ready (attempt $i)"
    exit 0
  fi
  echo "Waiting for Keycloak... ($i/$MAX_RETRIES)"
  sleep "$RETRY_INTERVAL"
done
echo "ERROR: Keycloak did not become ready in time"
exit 1

.env: Environment variables file template

# .env -- Keycloak Docker Compose environment variables
# NEVER commit this file to version control
DB_PASSWORD=change_me_to_a_strong_password
KC_ADMIN_USER=admin
KC_ADMIN_PASSWORD=change_me_to_a_strong_password
KC_HOSTNAME=https://auth.example.com

Anti-Patterns

Wrong: Using start-dev in production

# BAD -- start-dev disables HTTPS, uses H2 in-memory DB, enables insecure defaults
services:
  keycloak:
    image: quay.io/keycloak/keycloak:26.0
    command: start-dev
    environment:
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: admin

Correct: Using start --optimized with external database

# GOOD -- production mode with PostgreSQL and optimized startup
services:
  keycloak:
    image: quay.io/keycloak/keycloak:26.0
    command: start --optimized
    environment:
      KC_BOOTSTRAP_ADMIN_USERNAME: ${KC_ADMIN_USER}
      KC_BOOTSTRAP_ADMIN_PASSWORD: ${KC_ADMIN_PASSWORD}
      KC_DB: postgres
      KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak

Wrong: Using deprecated admin credential variables

# BAD -- KEYCLOAK_ADMIN/KEYCLOAK_ADMIN_PASSWORD deprecated in Keycloak 26+
environment:
  KEYCLOAK_ADMIN: admin
  KEYCLOAK_ADMIN_PASSWORD: admin

Correct: Using KC_BOOTSTRAP_ADMIN_ variables

# GOOD -- KC_BOOTSTRAP_ADMIN_ is the current standard (Keycloak 26+)
environment:
  KC_BOOTSTRAP_ADMIN_USERNAME: ${KC_ADMIN_USER}
  KC_BOOTSTRAP_ADMIN_PASSWORD: ${KC_ADMIN_PASSWORD}

Wrong: No health check or dependency ordering

# BAD -- Keycloak may start before PostgreSQL is ready
services:
  postgres:
    image: postgres:16-alpine
  keycloak:
    image: quay.io/keycloak/keycloak:26.0
    depends_on:
      - postgres

Correct: Health-check-based dependency with resource limits

# GOOD -- Keycloak waits for PostgreSQL readiness + memory limits
services:
  postgres:
    image: postgres:16-alpine
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U keycloak"]
      interval: 10s
      retries: 5
  keycloak:
    image: quay.io/keycloak/keycloak:26.0
    depends_on:
      postgres:
        condition: service_healthy
    deploy:
      resources:
        limits:
          memory: 2G

Wrong: Hardcoded secrets in docker-compose.yml

# BAD -- secrets in plain text (will be committed to git)
environment:
  KC_DB_PASSWORD: my_super_secret_password
  KC_BOOTSTRAP_ADMIN_PASSWORD: admin123

Correct: Using .env file or Docker secrets

# GOOD -- secrets from .env file (add to .gitignore)
environment:
  KC_DB_PASSWORD: ${DB_PASSWORD}
  KC_BOOTSTRAP_ADMIN_PASSWORD: ${KC_ADMIN_PASSWORD}

Common Pitfalls

Diagnostic Commands

# Check if Keycloak is healthy
curl -sf http://localhost:9000/health/ready | jq .

# View Keycloak startup logs
docker compose logs keycloak --tail=50

# Test OIDC discovery endpoint
curl -s http://localhost:8080/realms/master/.well-known/openid-configuration | jq .

# Export realm from running instance
docker exec keycloak /opt/keycloak/bin/kc.sh export \
  --dir /opt/keycloak/data/export --realm master

# Check PostgreSQL is accepting connections
docker exec postgres pg_isready -U keycloak

# Inspect Keycloak container environment
docker exec keycloak env | grep KC_

# Check resource usage
docker stats keycloak postgres --no-stream

# Verify TLS certificate (production)
openssl s_client -connect auth.example.com:443 -servername auth.example.com </dev/null 2>/dev/null | openssl x509 -noout -dates

Version History & Compatibility

VersionStatusBreaking ChangesMigration Notes
26.x (2024-2025)CurrentKC_BOOTSTRAP_ADMIN_* replaces KEYCLOAK_ADMIN_*; removed legacy WildFly configUpdate env vars; use Quarkus-native config
24.x (2024)MaintainedHostname V2 default; KC_PROXY deprecated for KC_PROXY_HEADERSReplace KC_PROXY=edge with KC_PROXY_HEADERS=xforwarded
22.x-23.x (2023-2024)EOLPersistent user sessions enabled by defaultReview session storage configuration
21.x (2023)EOLWildFly distribution removedMigrate to Quarkus distribution
17.x-20.x (2022-2023)EOLQuarkus becomes default; WildFly deprecatedSee migration guide
Pre-17 (WildFly)EOLCompletely different config formatFull migration required

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
Need self-hosted OIDC/SAML identity providerSimple API key authentication is sufficientAPI gateway with key management
Require fine-grained authorization (RBAC, ABAC)Only need social login (Google, GitHub)Firebase Auth, Supabase Auth
Multi-tenant application needing realm isolationSingle-tenant app with basic authSimple session-based auth
On-premise deployment required by complianceCloud-native and want zero infra managementAuth0, Okta, AWS Cognito
Need LDAP/AD federation with modern OIDC frontendAlready using cloud IAM (Azure AD, Google Workspace)Direct OIDC integration with cloud provider

Important Caveats

Related Units