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_PASSWORDpoint Keycloak at PostgreSQL instead of the embedded database. Run PostgreSQL with a persistent volume and its own backup.KC_HOSTNAMEis the public URL users and applications see. If it is wrong, redirect URLs and discovery documents are wrong too.KC_HTTP_ENABLED=trueis correct only when a reverse proxy terminates TLS and forwards to Keycloak. If Keycloak handles TLS itself, configure the keystore instead.KC_PROXY_HEADERS=xforwardedmakes Keycloak readX-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_ENABLEDandKC_METRICS_ENABLEDexpose health and metrics on the management port, 9000 by default./health/readytells a load balancer when the instance can serve traffic;/health/livetells it the process is alive.KC_BOOTSTRAP_ADMIN_USERNAMEandKC_BOOTSTRAP_ADMIN_PASSWORDcreate 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
- Back up the PostgreSQL database before every Keycloak upgrade.
- Read the upgrade notes for the target version; some releases change configuration or database schema.
- Run the new image against a copy of the database first.
- 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-devin production. - Using
latestand restarting into an untested major version. - Setting
KC_HOSTNAMEto 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-realmas 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.
Sources
- Keycloak: running in a container
- Keycloak: configuring the server
- Keycloak: configuring a reverse proxy
- Keycloak: health endpoints
- Keycloak on GitHub
Product names are trademarks of their respective owners.