---
# === IDENTITY ===
id: software/devops/docker-keycloak/2026
canonical_question: "Docker Compose reference: Keycloak"
aliases:
  - "Keycloak Docker Compose setup"
  - "run Keycloak in Docker with PostgreSQL"
  - "Keycloak container configuration"
  - "docker-compose.yml for Keycloak"
  - "Keycloak Quarkus Docker production"
  - "Keycloak OIDC Docker setup"
  - "self-hosted Keycloak with Docker"
  - "Keycloak realm import Docker Compose"
entity_type: software_reference
domain: software > devops > Docker Keycloak
region: global
jurisdiction: global
temporal_scope: 2024-2026

# === VERIFICATION ===
last_verified: 2026-02-27
confidence: 0.93
version: 1.0
first_published: 2026-02-27

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: stable
  last_breaking_change: "Keycloak 26.0 deprecated KEYCLOAK_ADMIN/KEYCLOAK_ADMIN_PASSWORD in favor of KC_BOOTSTRAP_ADMIN_USERNAME/KC_BOOTSTRAP_ADMIN_PASSWORD; Keycloak 24.0 removed legacy WildFly configuration format"
  next_review: 2026-08-26
  change_sensitivity: medium

# === CONSTRAINTS ===
constraints:
  - "NEVER run Keycloak with start-dev in production -- it disables HTTPS, uses H2 in-memory DB, and enables insecure defaults"
  - "Always use --optimized flag with a pre-built image in production to avoid runtime build overhead on every container start"
  - "Keycloak 26+ requires KC_BOOTSTRAP_ADMIN_USERNAME/KC_BOOTSTRAP_ADMIN_PASSWORD -- the older KEYCLOAK_ADMIN variables are deprecated"
  - "Production mode requires HTTPS configuration or KC_PROXY_HEADERS=xforwarded when behind a TLS-terminating reverse proxy"
  - "Set explicit memory limits (minimum 750 MB, recommended 2 GB) -- Keycloak will consume all available memory without limits"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "Need OAuth2/OIDC conceptual explanation, not deployment"
    use_instead: "software/patterns/oauth2-oidc/2026"
  - condition: "Using a managed Keycloak service (Auth0, Okta, AWS Cognito) instead of self-hosted"
    use_instead: "software/devops/managed-identity-providers/2026"
  - condition: "Need general Docker Compose syntax reference"
    use_instead: "software/devops/docker-compose-reference/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: "deployment_mode"
    question: "Is this for development/testing or production?"
    type: choice
    options: ["development", "production"]
  - key: "reverse_proxy"
    question: "Will Keycloak sit behind a reverse proxy (Nginx, Traefik, Caddy)?"
    type: choice
    options: ["yes", "no"]
  - key: "database"
    question: "Which database backend will you use?"
    type: choice
    options: ["PostgreSQL", "MySQL", "MariaDB"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/devops/docker-keycloak/2026"
suggested_citation: "Source: knowledgelib.io -- AI Knowledge Library (verified 2026-02-27)"

# === RELATED UNITS ===
related_kos:
  depends_on: []
  related_to:
    - id: "software/devops/docker-compose-reference/2026"
      label: "Docker Compose Reference"
    - id: "software/devops/docker-traefik/2026"
      label: "Docker Compose: Traefik Reverse Proxy"
  solves: []
  alternative_to: []
  often_confused_with:
    - id: "software/patterns/oauth2-oidc/2026"
      label: "OAuth2/OIDC Concepts (not deployment)"

# === SOURCES (7 authoritative sources) ===
# Types: official_docs, technical_blog, rfc_spec, academic_paper, community_resource, industry_report
# Reliability: high, moderate_high, moderate, moderate_low, low, authoritative
sources:
  - id: src1
    title: "Running Keycloak in a container"
    author: Keycloak Project
    url: https://www.keycloak.org/server/containers
    type: official_docs
    published: 2025-12-01
    reliability: authoritative
  - id: src2
    title: "Configuring Keycloak for production"
    author: Keycloak Project
    url: https://www.keycloak.org/server/configuration-production
    type: official_docs
    published: 2025-12-01
    reliability: authoritative
  - id: src3
    title: "Importing and exporting realms"
    author: Keycloak Project
    url: https://www.keycloak.org/server/importExport
    type: official_docs
    published: 2025-12-01
    reliability: authoritative
  - id: src4
    title: "Keycloak Using Docker and Docker Compose (2025 Edition)"
    author: MasterTheBoss
    url: https://www.mastertheboss.com/keycloak/keycloak-with-docker/
    type: technical_blog
    published: 2025-06-15
    reliability: moderate_high
  - id: src5
    title: "Configuring a reverse proxy"
    author: Keycloak Project
    url: https://www.keycloak.org/server/reverseproxy
    type: official_docs
    published: 2025-12-01
    reliability: authoritative
  - id: src6
    title: "Migrating to Quarkus distribution"
    author: Keycloak Project
    url: https://www.keycloak.org/migration/migrating-to-quarkus
    type: official_docs
    published: 2025-12-01
    reliability: authoritative
  - id: src7
    title: "Docker Compose Deployments for Keycloak and PostgreSQL"
    author: Akalanka Dissanayake
    url: https://medium.com/@disa2aka/docker-deployments-for-keycloak-and-postgresql-e75707b155e5
    type: technical_blog
    published: 2024-08-20
    reliability: moderate_high
---

# Docker Compose Reference: Keycloak

## TL;DR

- **Bottom line**: Keycloak (Quarkus-based) runs as `quay.io/keycloak/keycloak` with PostgreSQL backend, requiring separate `build` and `start --optimized` phases for production deployments.
- **Key tool/command**: `docker compose up -d` with a compose file defining `keycloak` + `postgres` services and KC_ environment variables.
- **Watch out for**: Using `start-dev` in production -- it disables HTTPS, uses an in-memory H2 database, and enables insecure defaults that will lose all data on restart.
- **Works with**: Keycloak 24.x-26.x (Quarkus distribution). Image: `quay.io/keycloak/keycloak:26.0`. Requires Docker Compose V2+.

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- NEVER run Keycloak with `start-dev` in production -- it disables HTTPS, uses H2 in-memory DB, and enables insecure defaults
- Always use `--optimized` flag with a pre-built image in production to avoid runtime build overhead on every container start
- Keycloak 26+ requires `KC_BOOTSTRAP_ADMIN_USERNAME`/`KC_BOOTSTRAP_ADMIN_PASSWORD` -- the older `KEYCLOAK_ADMIN` variables are deprecated
- Production mode requires HTTPS configuration or `KC_PROXY_HEADERS=xforwarded` when behind a TLS-terminating reverse proxy
- Set explicit memory limits (minimum 750 MB, recommended 2 GB) -- Keycloak will consume all available memory without limits

## Quick Reference

| Service | Image | Ports | Volumes | Key Env |
|---|---|---|---|---|
| keycloak | `quay.io/keycloak/keycloak:26.0` | `8080:8080` (dev), `8443:8443` (prod) | `./realm-export:/opt/keycloak/data/import` | `KC_DB=postgres` |
| keycloak (health) | (same) | `9000:9000` | (none) | `KC_HEALTH_ENABLED=true` |
| postgres | `postgres:16-alpine` | `5432:5432` | `pgdata:/var/lib/postgresql/data` | `POSTGRES_DB=keycloak` |

**Keycloak Environment Variables:**

| Variable | Purpose | Dev Default | Prod Recommendation |
|---|---|---|---|
| `KC_BOOTSTRAP_ADMIN_USERNAME` | Initial admin username | `admin` | Set via secret |
| `KC_BOOTSTRAP_ADMIN_PASSWORD` | Initial admin password | `admin` | Set via secret |
| `KC_DB` | Database vendor | `dev-file` (H2) | `postgres` |
| `KC_DB_URL` | JDBC connection URL | (auto) | `jdbc:postgresql://postgres:5432/keycloak` |
| `KC_DB_USERNAME` | Database user | (auto) | `keycloak` |
| `KC_DB_PASSWORD` | Database password | (auto) | Set via secret |
| `KC_HOSTNAME` | Public-facing hostname | (none) | `https://auth.example.com` |
| `KC_PROXY_HEADERS` | Proxy header forwarding | (none) | `xforwarded` |
| `KC_HTTP_ENABLED` | Allow plain HTTP | `true` | `true` (behind proxy) or `false` |
| `KC_HEALTH_ENABLED` | Health check endpoints | `false` | `true` |
| `KC_METRICS_ENABLED` | Prometheus metrics | `false` | `true` |
| `KC_LOG_LEVEL` | Logging verbosity | `info` | `info` or `warn` |
| `KC_FEATURES` | Enable preview features | (none) | As needed |

**Health & Readiness Endpoints (port 9000):**

| Endpoint | Purpose | Expected Response |
|---|---|---|
| `/health` | Overall health | `{"status": "UP"}` |
| `/health/ready` | Readiness probe (K8s) | `{"status": "UP"}` |
| `/health/live` | Liveness probe (K8s) | `{"status": "UP"}` |
| `/metrics` | Prometheus metrics | Prometheus text format |

## Decision Tree

```
START: What deployment mode do you need?
├── Development / local testing?
│   ├── YES → Use start-dev with H2 (no database service needed)
│   │         See: Development Compose below
│   └── NO ↓
├── Production single-node?
│   ├── YES → Use optimized build + PostgreSQL + reverse proxy
│   │         See: Production Compose below
│   └── NO ↓
├── Production cluster (HA)?
│   ├── YES → Add KC_CACHE=ispn, open JGroups ports (7800), use shared DB
│   │         See: Clustering section in official docs
│   └── NO ↓
├── Need realm pre-configuration?
│   ├── YES → Mount realm JSON to /opt/keycloak/data/import + --import-realm
│   │         See: Step 4 (Realm Import)
│   └── NO ↓
├── Behind reverse proxy (Nginx/Traefik/Caddy)?
│   ├── YES → Set KC_PROXY_HEADERS=xforwarded + KC_HTTP_ENABLED=true
│   │         See: Step 3 (Reverse Proxy)
│   └── 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]

```yaml
# 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 output: `"master"`

### 2. Create production Docker Compose with PostgreSQL

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

```yaml
# 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 example)

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

```nginx
# nginx.conf -- reverse proxy for Keycloak
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` with proper `Content-Type: application/json`

### 4. Import a realm on startup

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

```yaml
# Add to keycloak service in docker-compose.yml
services:
  keycloak:
    command: start --optimized --import-realm
    volumes:
      - ./realm-export.json:/opt/keycloak/data/import/realm-export.json:ro
```

Export an existing realm:
```bash
# 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

For production, create a custom Dockerfile that runs the `build` step at image build time rather than at container startup. This dramatically reduces startup time. [src1]

```dockerfile
# Dockerfile.keycloak
FROM quay.io/keycloak/keycloak:26.0 AS builder

# Configure build-time options
ENV KC_DB=postgres
ENV KC_HEALTH_ENABLED=true
ENV KC_METRICS_ENABLED=true
ENV KC_FEATURES=token-exchange,admin-fine-grained-authz

# Build optimized Keycloak
RUN /opt/keycloak/bin/kc.sh build

# Production image
FROM quay.io/keycloak/keycloak:26.0
COPY --from=builder /opt/keycloak/ /opt/keycloak/

ENTRYPOINT ["/opt/keycloak/bin/kc.sh"]
```

```yaml
# docker-compose.yml with custom build
services:
  keycloak:
    build:
      context: .
      dockerfile: Dockerfile.keycloak
    command: start --optimized
    # ... rest of config
```

**Verify**: Container starts in <5 seconds (vs 30-60s without optimization). Check logs: `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]

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

```dockerfile
# Dockerfile approach (production)
FROM quay.io/keycloak/keycloak:26.0 AS builder
COPY themes/my-theme /opt/keycloak/themes/my-theme
ENV KC_DB=postgres
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**: Log in to Admin Console --> Realm Settings --> Themes --> select "my-theme" from dropdown

## Code Examples

### YAML: Complete production docker-compose.yml with all services

> Full script: [production-docker-compose.yml](scripts/production-docker-compose.yml) (62 lines)

```yaml
# 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 for automated provisioning

> Full script: [realm-export-example.json](scripts/realm-export-example.json) (48 lines)

```json
{
  "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

```bash
#!/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

```bash
# .env -- Keycloak Docker Compose environment variables
# NEVER commit this file to version control

# Database
DB_PASSWORD=change_me_to_a_strong_password

# Keycloak Admin
KC_ADMIN_USER=admin
KC_ADMIN_PASSWORD=change_me_to_a_strong_password

# Hostname (set to your actual domain)
KC_HOSTNAME=https://auth.example.com
```

## Anti-Patterns

### Wrong: Using start-dev in production

```yaml
# 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

```yaml
# 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

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

### Correct: Using KC_BOOTSTRAP_ADMIN_ variables

```yaml
# 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

```yaml
# BAD -- Keycloak may start before PostgreSQL is ready, causing connection errors
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

```yaml
# GOOD -- Keycloak waits for PostgreSQL readiness + explicit 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

```yaml
# BAD -- secrets in plain text in compose file (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

```yaml
# GOOD -- secrets from .env file (add to .gitignore)
environment:
  KC_DB_PASSWORD: ${DB_PASSWORD}
  KC_BOOTSTRAP_ADMIN_PASSWORD: ${KC_ADMIN_PASSWORD}
# Or use Docker secrets for Swarm/production:
# secrets:
#   - db_password
```

## Common Pitfalls

- **start without --optimized takes 30-60s**: Running `start` without a prior `build` step causes Keycloak to rebuild its configuration at every container startup. Fix: Use a multi-stage Dockerfile with `kc.sh build` or always pass `--optimized`. [src1]
- **H2 database data loss on restart**: Default `start-dev` uses an in-memory H2 database. All realms, users, and clients are lost when the container restarts. Fix: Always configure `KC_DB=postgres` with a persistent volume for the database. [src2]
- **Admin credentials only work on first boot**: `KC_BOOTSTRAP_ADMIN_USERNAME`/`KC_BOOTSTRAP_ADMIN_PASSWORD` only create the admin user on initial startup. Changing them after the first boot has no effect. Fix: Use the Admin Console or Admin CLI to change credentials after initial setup. [src1]
- **OIDC client secrets missing from realm export**: The standard realm export omits confidential client secrets. Dependent services fail to authenticate. Fix: Manually add `"secret": "your-secret"` to the client entry in the exported JSON, or use the Admin REST API to export with secrets. [src3]
- **Reverse proxy buffer too small**: Default Nginx proxy buffer sizes cause `502 Bad Gateway` errors during OIDC token exchanges with large JWTs. Fix: Set `proxy_buffer_size 128k` and `proxy_buffers 4 256k` in Nginx configuration. [src5]
- **Port 9000 not exposed for health checks**: Health and metrics endpoints run on port 9000, separate from the main 8080/8443 port. Container orchestrators cannot perform readiness checks. Fix: Expose port 9000 and configure healthcheck against `/health/ready`. [src1]
- **KC_HOSTNAME mismatch causes redirect loops**: Setting `KC_HOSTNAME` to a value that does not match the actual proxy hostname causes infinite redirects. Fix: Ensure `KC_HOSTNAME` exactly matches the external URL including protocol (e.g., `https://auth.example.com`). [src5]

## Diagnostic Commands

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

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

# Check database connectivity from Keycloak container
docker exec keycloak /opt/keycloak/bin/kc.sh show-config | grep db

# 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

| Version | Status | Breaking Changes | Migration Notes |
|---|---|---|---|
| 26.x (2024-2025) | Current | `KC_BOOTSTRAP_ADMIN_*` replaces `KEYCLOAK_ADMIN_*`; removed legacy WildFly config format | Update env vars; use Quarkus-native config |
| 24.x (2024) | Maintained | Hostname V2 becomes default; KC_PROXY deprecated in favor of KC_PROXY_HEADERS | Replace `KC_PROXY=edge` with `KC_PROXY_HEADERS=xforwarded` |
| 22.x-23.x (2023-2024) | EOL | Persistent user sessions enabled by default | Review session storage configuration |
| 21.x (2023) | EOL | WildFly distribution removed | Migrate to Quarkus distribution |
| 17.x-20.x (2022-2023) | EOL | Quarkus distribution becomes default; WildFly deprecated | See migration guide (src6) |
| Pre-17 (WildFly) | EOL | Completely different config format | Full migration required via src6 |

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Need self-hosted OIDC/SAML identity provider | Simple API key authentication is sufficient | API 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 isolation | Single-tenant app with basic auth | Simple session-based auth |
| On-premise deployment required by compliance | Cloud-native and want zero infra management | Auth0, Okta, AWS Cognito |
| Need LDAP/AD federation with modern OIDC frontend | Already using cloud IAM (Azure AD, Google Workspace) | Direct OIDC integration with cloud provider |

## Important Caveats

- Keycloak's Quarkus distribution requires a separate `build` step that bakes database driver selection, feature flags, and cache configuration into the binary -- changing these at runtime requires rebuilding
- The `--import-realm` flag processes files in `/opt/keycloak/data/import` only on first startup or when the realm does not exist -- it will NOT overwrite existing realms
- Client secrets are NOT included in standard realm exports for security reasons -- you must manually add them or use the Admin REST API with appropriate permissions
- Memory consumption scales with active session count -- a deployment with 10,000+ concurrent sessions may need 4+ GB RAM regardless of the 2 GB baseline recommendation
- Docker Compose alone does not provide HA -- for production clustering, use `KC_CACHE=ispn` with JGroups discovery and a shared database, or deploy on Kubernetes

## Related Units
<!-- Generated from related_kos frontmatter -->

- [Docker Compose Reference](/software/devops/docker-compose-reference/2026)
- [Docker Compose: Traefik Reverse Proxy](/software/devops/docker-traefik/2026)
- [OAuth2/OIDC Concepts](/software/patterns/oauth2-oidc/2026)
