Production Checklist
From start-dev to production
Section titled “From start-dev to production”start-dev mode exists only for local development. It disables the optimised build, uses an embedded H2 database, generates a self-signed certificate, and relaxes hostname validation. None of these defaults are safe in production. When you are ready to go live, there are four things you must address.
The four production must-haves
Section titled “The four production must-haves”1. External database
Section titled “1. External database”By default, start-dev uses an embedded H2 database that lives inside the container. Restart the container and all data is gone. In production, point Keycloak at an external database — PostgreSQL is the recommended choice.
Set these environment variables:
KC_DB=postgresKC_DB_URL=jdbc:postgresql://db-host:5432/keycloakKC_DB_USERNAME=keycloakKC_DB_PASSWORD=<secret>2. Fixed hostname
Section titled “2. Fixed hostname”Keycloak embeds its own hostname into token issuers, OIDC discovery documents, and redirect validation. If the hostname is wrong, tokens are rejected by clients. Set it explicitly:
KC_HOSTNAME=https://auth.example.comKeycloak 26 replaced the old hostname options with hostname v2. The headline difference is that KC_HOSTNAME now accepts a full base URL, including scheme, port, and context path — https://auth.example.com:8443/auth is valid. Passing a bare hostname still works, in which case the scheme and port are resolved from the request.
Two related options are worth knowing:
KC_HOSTNAME_ADMIN— serve the admin console on a different URL, so you can keep it off the public one.KC_HOSTNAME_STRICT=false— let Keycloak resolve the hostname from request headers. Useful for local testing behind a proxy, never in production.
If you find KC_HOSTNAME_URL, KC_HOSTNAME_PORT, or KC_HOSTNAME_PATH in an older guide, those are hostname v1 and were removed in 26.
3. HTTPS and reverse proxy
Section titled “3. HTTPS and reverse proxy”Keycloak must be served over HTTPS. You have two common options:
-
Edge termination (recommended): A reverse proxy (Nginx, Traefik, AWS ALB) handles TLS and forwards plain HTTP to Keycloak. Tell Keycloak to trust the proxy headers:
KC_PROXY_HEADERS=xforwarded -
Passthrough: Keycloak handles TLS itself. Provide a certificate and private key via
KC_HTTPS_CERTIFICATE_FILEandKC_HTTPS_CERTIFICATE_KEY_FILE.
4. Optimised build
Section titled “4. Optimised build”In production, run kc.sh build before kc.sh start. The build step compiles provider configuration, augments the Quarkus app, and caches classpath scanning — reducing startup time from ~30 s to ~3 s. Most Docker deployments do this in a multi-stage Dockerfile:
# Build stagekc.sh build --db=postgres
# Runtime stagekc.sh start --optimizedSessions survive restarts
Section titled “Sessions survive restarts”Since Keycloak 25, persistent user sessions are the default: sessions are written to the database and loaded into the cache on demand. Restart a node — or the whole cluster — and users stay logged in. This is a behaviour change worth knowing if you remember older versions, where a restart logged everybody out.
You can turn it off for a cache-only deployment:
KC_FEATURES_DISABLED=persistent-user-sessionsDo not do that casually. Volatile sessions lose every session when the last node restarts, and they cannot be combined with the multi-site feature.
A production docker run
Section titled “A production docker run”Here is a minimal docker run that combines all four must-haves:
docker run -d --name keycloak \
-p 8080:8080 \
-e KC_DB=postgres \
-e KC_DB_URL=jdbc:postgresql://db:5432/keycloak \
-e KC_DB_USERNAME=keycloak \
-e KC_DB_PASSWORD=${DB_PASSWORD} \
-e KC_HOSTNAME=auth.example.com \
-e KC_PROXY_HEADERS=xforwarded \
-e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
-e KC_BOOTSTRAP_ADMIN_PASSWORD=${ADMIN_PASSWORD} \
quay.io/keycloak/keycloak:latest \
start --optimizedPass DB_PASSWORD and ADMIN_PASSWORD from your secrets manager — never hardcode them in the command.
Config-as-code with realm export/import
Section titled “Config-as-code with realm export/import”Manual admin-console clicks are not repeatable. Export your realm configuration as JSON and commit it to version control:
# Export a realm/opt/keycloak/bin/kc.sh export --realm my-app --file /tmp/my-app-realm.json
# Import on startup/opt/keycloak/bin/kc.sh start --import-realmPlace my-app-realm.json in /opt/keycloak/data/import/ and Keycloak will import it automatically on first start. This makes realm configuration reproducible across environments.
Backup strategy
Section titled “Backup strategy”Keycloak’s state lives entirely in the external database. Back up the database using your database provider’s standard backup mechanism. The container itself is stateless and does not need to be backed up — just ensure the realm export JSON is in version control as a configuration baseline.
Trade-offs
Section titled “Trade-offs”| Option | Benefit | Cost |
|---|---|---|
| Edge TLS termination (reverse proxy) | Manages the certificate in one place (Nginx/Traefik/ALB) and supports easy rolling cert renewal | KC_PROXY_HEADERS must be configured correctly, or Keycloak sees the wrong protocol/host and redirects break |
| TLS passthrough (Keycloak handles it directly) | No extra proxy dependency — simpler for a minimal deployment | You manage the certificate lifecycle yourself, on every Keycloak instance |
Optimised build (kc.sh build ahead of time) | Cuts startup time from ~30 seconds to ~3 seconds — good for containers that scale up and down often | Adds a separate build step to CI/CD, and the image must be rebuilt every time a provider config changes |
Common mistakes
Section titled “Common mistakes”- Running
start-devin production — it still uses the embedded H2 database, a self-signed certificate, and relaxed hostname checks, so data is lost on every container restart and several security gaps open at once. - Exposing
/metricsor/healthpublicly with no access restriction — these endpoints reveal internal details about the system and should only be reachable from your internal network or monitoring system. - Not setting
KC_HOSTNAMEto match the real domain — this makes the token issuer and OIDC discovery document not match what clients expect, so clients reject tokens even though Keycloak itself is working fine.
Real-world example
A platform team’s standard production checklist — most infrastructure teams wire external PostgreSQL, a fixed hostname via DNS, TLS via a reverse proxy, and multi-node clustering together into a single deployment template, then use realm export/import to reproduce the same configuration in every environment.
High-availability clustering — organisations that need high uptime run several Keycloak instances behind a load balancer, sharing one external database, so that losing any single instance doesn’t log users out.