Skip to content

Production Checklist

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.

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=postgres
KC_DB_URL=jdbc:postgresql://db-host:5432/keycloak
KC_DB_USERNAME=keycloak
KC_DB_PASSWORD=<secret>

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.com

Keycloak 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.

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_FILE and KC_HTTPS_CERTIFICATE_KEY_FILE.

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:

Terminal window
# Build stage
kc.sh build --db=postgres
# Runtime stage
kc.sh start --optimized

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-sessions

Do not do that casually. Volatile sessions lose every session when the last node restarts, and they cannot be combined with the multi-site feature.

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 --optimized

Pass DB_PASSWORD and ADMIN_PASSWORD from your secrets manager — never hardcode them in the command.

Manual admin-console clicks are not repeatable. Export your realm configuration as JSON and commit it to version control:

Terminal window
# 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-realm

Place 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.

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.

OptionBenefitCost
Edge TLS termination (reverse proxy)Manages the certificate in one place (Nginx/Traefik/ALB) and supports easy rolling cert renewalKC_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 deploymentYou 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 oftenAdds a separate build step to CI/CD, and the image must be rebuilt every time a provider config changes
  • Running start-dev in 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 /metrics or /health publicly 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_HOSTNAME to 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.

What happens to user sessions when you restart Keycloak 26 with default settings?
What database does start-dev use by default?
What does KC_PROXY_HEADERS=xforwarded tell Keycloak?
Why should you run kc.sh build before kc.sh start in production?
How do you make Keycloak automatically import a realm on first startup?