Skip to main content

Deploying Keycloak

Pattern: standing up the identity provider itself. Everything else in this book assumes a working Keycloak that applications can reach over HTTPS and that you can administer safely. This page covers the deployment;deployment and the next covers configuring the realmrealms inside it.

The deployment below is written as a standalone Keycloak:Keycloak itinstance. It terminates its own TLS and is reachable directly. If you run it behind a reverse proxy or web application firewall, as thismy environment does, see the note at the end of the page, because several settings change.

The shape of the deployment

Keycloak runs in a container, backed byand a PostgreSQL database,database run in containers with its data and configuration on the host so that backups and upgrades are straightforward. Three decisions drive everything else:

  • Containerized, with a pinned version. Keycloak sometimes introduces breaking changes between minor versions often enough that :image:latest is a genuine operational hazard. The image tag isshould alwaysuse explicit.an explicit image.
  • An external database, not the embedded one. Keycloak's development-mode database is not meant for real use. PostgreSQL in its own container, with its data persisted on the host, is the minimum viable production setup.
  • Its own hostname over TLS. Applications and browsers both need to reach the identity provider at a stable HTTPS URL, and that URL ends up baked into every client's discovery configuration.

Compose file

services:
  postgres:
    image: postgres:16
    container_name: keycloak-postgres
    environment:
      - POSTGRES_DB=keycloak
      - POSTGRES_USER=keycloak
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
    volumes:
      - /opt/apps/keycloak/postgres:/var/lib/postgresql/data
    restart: unless-stopped

  keycloak:
    image: quay.io/keycloak/keycloak:26.5.7     # pinned; never :latest
    container_name: keycloak
    command: start
    environment:
      - KC_DB=postgres
      - KC_DB_URL=jdbc:postgresql://postgres:5432/keycloak
      - KC_DB_USERNAME=keycloak
      - KC_DB_PASSWORD=${POSTGRES_PASSWORD}
      - KC_HOSTNAME=https://auth.example.org
      - KC_HTTPS_CERTIFICATE_FILE=/etc/certs/fullchain.pem
      - KC_HTTPS_CERTIFICATE_KEY_FILE=/etc/certs/privkey.pem
      - KC_BOOTSTRAP_ADMIN_USERNAME=${KC_ADMIN}
      - KC_BOOTSTRAP_ADMIN_PASSWORD=${KC_ADMIN_PASSWORD}
    volumes:
      - /opt/apps/keycloak/certs:/etc/certs:ro
      - /opt/apps/keycloak/themes:/opt/keycloak/themes
    ports:
      - "443:8443"
    depends_on:
      - postgres
    restart: unless-stopped

Secrets come from an .env file beside the compose file, never inline:

POSTGRES_PASSWORD=REDACTED
KC_ADMIN=REDACTED
KC_ADMIN_PASSWORD=REDACTED

What each setting does, and why

Setting Why it's set this way
image: …:26.5.7 Pinned to an explicit version. Keycloak upgrades can require database migrations and can change realm or client behavior, so version changes are deliberate maintenance events, not something that happens because a container restarted.
command: start Production mode. The alternative, start-dev, disables TLS requirements and uses an ephemeral database. It is for local experimentation only and should never appear on a host that real users authenticate against.
KC_DB / KC_DB_URL / credentials Points Keycloak at PostgreSQL. The database holds every realm, user, client, and session, which makes it the single most important thing to back up.
KC_HOSTNAME The public URL Keycloak believes it lives at. This is not cosmetic: it is used to build the issuer value, the endpoints published in the discovery document, and the redirect targets. If it is wrong, clients receive URLs they cannot reach, or token issuer validation fails.
KC_HTTPS_CERTIFICATE_FILE / _KEY_FILE TLS served by Keycloak itself, in the standalone arrangement.
KC_BOOTSTRAP_ADMIN_* Creates the initial administrator on first startup only. Treat these as bootstrap credentials: use them once to log in, then create a proper administrator account with MFA, and remove these variables.
volumes: certs (ro) Certificates mounted read-only. The container needs to read them and has no reason to write them.
ports: 443:8443 Keycloak listens on 8443 inside the container; it is published on 443 on the host.

Certificates

In the standalone arrangement Keycloak needs a certificate for its own hostname. A containerized ACME client can obtain and renew it into the same directory the Keycloak container mounts:

docker run --rm \
  -v /opt/apps/keycloak/certbot-etc:/etc/letsencrypt \
  -v /opt/apps/keycloak/certbot-var:/var/lib/letsencrypt \
  certbot/certbot certonly --webroot \
  --webroot-path=/var/lib/letsencrypt \
  --email you@example.org --agree-tos --no-eff-email \
  -d auth.example.org

Renewal runs on a schedule:

1 0 5 * * docker run --rm -v /opt/apps/keycloak/certbot-etc:/etc/letsencrypt -v /opt/apps/keycloak/certbot-var:/var/lib/letsencrypt certbot/certbot renew --webroot --webroot-path /var/lib/letsencrypt -v

Keycloak reads its certificate at startup, so a renewal needs the container restarted to be picked up. Whatever automation renews the certificate should also restart the container, or a successfully renewed certificate will sit on disk unused while the running instance serves an expired one.

First login and locking down the admin account

  1. Browse to the Keycloak URL and sign in with the bootstrap administrator.
  2. Create a real administrative user with a strong, unique password stored in a password manager.
  3. Enable multi-factor authentication on that account immediately. The Keycloak administrator can create clients, alter authentication flows, and grant themselves access to every downstream application. It is the most privileged account in the environment and should never be protected by a password alone.
  4. Remove the bootstrap credentials from the environment file and restart.

Upgrades

Because the version is pinned, upgrades are a deliberate procedure rather than a background event. The sequence that has proven necessary:

  1. Back up the database first. pg_dump the Keycloak database to a file before anything else. Realms, users, clients, and mappers all live there; a failed migration without a backup is an environment-ending event.
  2. Back up configuration, including the compose file, the environment file, themes, and certificates.
  3. Read the upgrade notes for every version you are crossing, not just the target. Breaking changes accumulate across intermediate versions, and multi-version jumps sometimes need to be done in stages.
  4. Update the pinned tag, pull, and restart, watching the container logs as it comes up. Database migrations run at startup and their success or failure appears there.
  5. Test a real login end to end through at least one downstream application before considering the upgrade finished. Realm and client behavior can change subtly across versions in ways the container's own health check will not catch.

Keycloak's own upgrade documentation is the authority on breaking changes and is worth reading in full each time, not skimmed.

Backups

Two things must be captured for a restorable identity provider:

  • The database dump, which contains every realm, user, group, client, and mapper.
  • The configuration and secrets, meaning the compose file, environment file, certificates, and any custom themes.

A database backup without the environment file is not a restore, because the database credentials and admin bootstrap values live there. Both belong in the same backup routine as the rest of the environment.

Running behind a reverse proxy or WAF

This environment does not expose Keycloak directly. It sits behind a web application firewall that terminates public TLS and forwards to the Keycloak host over an internally authenticated connection. That arrangement changes several things above:

  • TLS terminates at the proxy, not at Keycloak. The public certificate lives on the proxy. Keycloak still needs to know its public URL through KC_HOSTNAME, because that is what it publishes in the discovery document and uses to build redirects, but it is no longer the component presenting the public certificate to browsers.
  • Keycloak must be told it is behind a proxy. Keycloak needs to trust the forwarded headers so it can construct correct URLs and see the real client address rather than the proxy's. Without this, redirects can be built with the internal hostname or the wrong scheme, and login loops or "invalid redirect" errors follow. The proxy must be configured to send the standard forwarded headers, and Keycloak configured to honor them.
  • The forwarded protocol header matters more than it looks. If the proxy does not tell Keycloak that the original request was HTTPS, Keycloak can build http:// URLs and browsers will refuse to follow them. This is one of the most common causes of a Keycloak deployment that works directly but breaks behind a proxy.
  • Certificate management moves. The public certificate is obtained and renewed on the proxy. Internally, the connection from the proxy to the Keycloak host is authenticated separately, which in this environment means mutual TLS with certificates from a private certificate authority.
  • Client IP visibility. Brute-force detection and logging are only as good as the address Keycloak sees. If the forwarded-for header is not passed and honored, every login attempt appears to come from the proxy, and per-address protections become meaningless.

A dedicated book on running a web application firewall in a small ecosystem is planned, covering the proxy configuration, request inspection, and the internal authentication between the proxy and its backends in detail. [Link to be added: WAF for Small Ecosystems]

Adapt this for…

Any self-hosted identity provider deployment. The specifics of the compose file are Keycloak's, but the decisions generalize: pin the version because identity providers break in ways that matter, use a real database and back it up as the crown jewels, make sure the service knows its own public URL, and protect the administrative account with multi-factor authentication before you do anything else with it. If the identity provider is compromised, every application behind it is compromised at the same moment, which is the entire reason the rest of this book works.