In Docker Compose, a Keycloak service needs command: start-dev, or start in production: the image has no default command, and without one kc.sh prints its usage and exits. The first admin comes from KC_BOOTSTRAP_ADMIN_USERNAME and KC_BOOTSTRAP_ADMIN_PASSWORD.
The docker run command, converted
The command from Keycloak’s “Get started with Docker” guide, with the release series pinned:
docker run -p 127.0.0.1:8080:8080 -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:26.8 start-dev
services:
keycloak:
image: quay.io/keycloak/keycloak:26.8
command: start-dev
ports:
- "127.0.0.1:8080:8080"
environment:
KC_BOOTSTRAP_ADMIN_USERNAME: admin
KC_BOOTSTRAP_ADMIN_PASSWORD: admin
start-dev keeps realms and users in an H2 database under /opt/keycloak/data/h2, inside the container, so they’re gone with it. A volume at /opt/keycloak/data keeps them; Postgres is the setup to grow into.
With Postgres and a health check
services:
postgres:
image: postgres:18
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: keycloak-db-pw
volumes:
- keycloak-db:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -h localhost -U keycloak -d keycloak"]
interval: 5s
timeout: 5s
retries: 10
keycloak:
image: quay.io/keycloak/keycloak:26.8
command: start-dev
ports:
- "127.0.0.1:8080:8080"
environment:
KC_BOOTSTRAP_ADMIN_USERNAME: admin
KC_BOOTSTRAP_ADMIN_PASSWORD: admin
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak
KC_DB_USERNAME: keycloak
KC_DB_PASSWORD: keycloak-db-pw
KC_HEALTH_ENABLED: "true"
healthcheck:
test: ["CMD-SHELL", "exec 3<>/dev/tcp/localhost/9000 && printf 'GET /health/ready HTTP/1.1\\r\\nHost: localhost\\r\\nConnection: close\\r\\n\\r\\n' >&3 && head -n 1 <&3 | grep -q ' 200 '"]
interval: 10s
timeout: 5s
retries: 10
start_period: 60s
depends_on:
postgres:
condition: service_healthy
volumes:
keycloak-db:
KC_HEALTH_ENABLED turns on /health/ready on the management port, 9000, which answers 200 when Keycloak is ready and 503 when a readiness check, such as the database connection, fails. The image is kept small and ships no curl or wget, but it has bash, so the check writes the HTTP request to /dev/tcp itself and reads the status line. Port 9000 needs no ports entry: the check runs inside the container.
Gotchas
- The bootstrap admin is temporary, as the log says (
Created temporary admin user with username admin): create a permanent admin and delete it. It’s only created while the master realm has no admin, so changingKC_BOOTSTRAP_ADMIN_PASSWORDlater does nothing. KEYCLOAK_ADMINandKEYCLOAK_ADMIN_PASSWORDfrom older guides still work in 26.8, with a warning:Environment variable 'KEYCLOAK_ADMIN' is deprecated, use 'KC_BOOTSTRAP_ADMIN_USERNAME' instead.command: startis production mode. It stops withKey material not provided to setup HTTPSuntil it has a certificate, orKC_HTTP_ENABLED: "true"behind a proxy that terminates TLS, and then withhostname is not configureduntilKC_HOSTNAMEis set.docker compose down -vdeleteskeycloak-db, with every realm in it.