Skip to content

Keycloak

Keycloak gives your applications one login: they redirect to it, it authenticates the user, and hands back a token the application trusts. It speaks OpenID Connect and SAML, federates users from LDAP and Active Directory, and manages realms, clients, roles and users in an admin console. Two containers: the server and PostgreSQL.

Last run on a host 8 Nov 2026

Pinned at 26.8.2 (released 5 Nov 2026) · the newest upstream release

In the upstream images: 1 CRITICAL · 26 HIGH with a published fix — per image

To try it: Docker, nothing else. See Try it locally.

To deploy it behind the proxy: Traefik running and a domain.

On your own machine, with no proxy, domain or certificate. From core/keycloak/:

Terminal window
cp .env.local.example .env.local
# The secrets ship empty; generate the ones this stack needs
{
echo "DB_PASSWORD=$(openssl rand -hex 32)"
echo "BOOTSTRAP_ADMIN_PASSWORD=$(openssl rand -base64 24 | tr -d '\n')"
} >> .env.local
mkdir -p volumes/local/postgres volumes/local/data
sudo chown 1000:0 volumes/local/data
docker compose -p keycloak-local -f docker-compose.local.yml --env-file .env.local up -d

The first start takes about 30 seconds. Open http://localhost:8080/admin and sign in as the bootstrap administrator. The explicit project name in the command keeps this apart from a production stack in the same directory.

For a deployment on a server. From core/keycloak/:

  1. Copy the environment file and set APP_TRAEFIK_HOST and TZ:

    Terminal window
    cp .env.example .env

    The hostname is the issuer. Every token names it, and Keycloak refuses requests for any other host.

  2. Generate the secrets and the data directory:

    Terminal window
    mkdir -p .secrets volumes/postgres volumes/data
    openssl rand -hex 32 | tr -d '\n' > .secrets/db_pwd.txt
    openssl rand -base64 24 | tr -d '\n' > .secrets/bootstrap_admin_pwd.txt
    chmod 700 .secrets && chmod 600 .secrets/*.txt
    sudo chown 1000:0 volumes/data
  3. Start it:

    Terminal window
    docker compose up -d
  4. Open https://<APP_TRAEFIK_HOST>/admin and sign in as temp-admin with the password from step 2. Create a permanent administrator in the master realm, sign in as that user, and delete the temporary one.

  5. Create a realm for your applications. The master realm administers Keycloak itself.

Terminal window
docker compose ps # both healthy
curl -fsS https://<APP_TRAEFIK_HOST>/realms/master/.well-known/openid-configuration | grep -o '"issuer":"[^"]*"'

The issuer must be https://<APP_TRAEFIK_HOST>/realms/master. A different scheme or host means Traefik’s forwarded headers are not reaching the server.

In your realm, Clients → Create client: OpenID Connect, the application’s redirect URI, client authentication on. The application gets three values — the issuer URL https://<APP_TRAEFIK_HOST>/realms/<realm>, the client ID and the secret — and reads everything else from the discovery document.

That is the whole integration, and also its limit: Keycloak has no proxy of its own. An application that speaks neither OpenID Connect nor SAML cannot be put behind it with what this stack ships. This blueprint also ships Authentik for the same role — for a side-by-side on containers, memory, federation and ecosystem, see Choosing between services → Single sign-on.

The database holds everything: realms, clients, users, sessions. volumes/data is staging for imports and exports and is otherwise empty. Restore the database, then start the container — it migrates the schema at boot and refuses a schema newer than itself. kc.sh export gives a realm-level JSON as a second, portable copy; the README has the borgmatic block.

  1. Read the release notes and the upgrading guide. A minor release may migrate the schema and rename options.

  2. Back up the database, then raise APP_TAG in .env and:

    Terminal window
    docker compose pull && docker compose up -d
    docker compose logs keycloak-app | grep -E 'started in|Migrat'
  3. Sign in, and let one application authenticate.

The container exits and the log ends with no password was provided. A KC_*_FILE variable was used. Keycloak accepts them into its configuration and ignores them; the entrypoint wrapper is how the passwords arrive.

The discovery document shows http:// or the wrong host. The server is not receiving X-Forwarded-Proto and X-Forwarded-Host. Through Traefik they are always present; through anything else, set the same headers.

The admin console stops at “Timeout when waiting for 3rd party check iframe message”. The browser console adds that the page refused to load in a frame because X-Frame-Options is deny. The console frames its cookie check from its own origin, and Keycloak sets the frame headers itself. The stack therefore ships headers-application-owned: the proxy sets the HSTS base only, and Keycloak’s own headers reach the browser; the one it lacks, Permissions-Policy, is the header-permissions@file block the compose file places before the profile. If the message appears, the router’s APP_TRAEFIK_HEADERS has been set to headers-standard, whose DENY overwrites Keycloak’s header on every page.

/health/ready is DOWN for a minute after start. The rebuild at every boot takes that long. The healthcheck allows 90 seconds.

The container exits with Read-only file system. read_only: true was added. The official image writes to its own lib/quarkus at start and cannot run that way.