Running Keycloak 17+ as Docker Container

Keycloak 17+ is not based on Wildfly anymore but uses Quarkus. This makes it a first-class citizen for running it as a Docker container. Quarkus reduces the startup time of Keycloak massively and reduces its memory footprint. Previously a Keycloak Docker container based on Wildfly consumed around ~800 MB RAM and took roughly 30 seconds to be up and running. Keycloak 17+ (Codename Keycloak.X) changes this by consuming ~300MB RAM and starts almost instantly.

Therefore I was super keen on giving the new Keycloak setup a try and run it on my local machine as a Docker container. This article is about my failures and the eventual success.

My main goal is to run Keycloak, as mentioned as a Docker container connected to a MariaDB database. As mentioned in the Keycloak documentation it is highly recommended to build your own optimised Keycloak Docker image.

For the best start up of your Keycloak container, build an image by running the build step during the container build. This step will save time in every subsequent start phase of the container image.

Easy, let's give it a try

So I started with a MariaDB and a optimised Keycloak Docker image, that listens on http port 8080. In my first attempt I build the optimised Keycloak image using the following Dockerfile.

FROM quay.io/keycloak/keycloak:18.0.0 as builder

ENV KC_DB=mariadb
RUN /opt/keycloak/bin/kc.sh build

FROM quay.io/keycloak/keycloak:18.0.0
COPY --from=builder /opt/keycloak/ /opt/keycloak/
WORKDIR /opt/keycloak
ENTRYPOINT ["/opt/keycloak/bin/kc.sh", "start"]

In the first phase I use the base image quay.io/keycloak/keycloak:18.0.0 to build my optimised Keycloak setup, declaring that I want to use MariaDB. In the second stage I pack the optimised output of the builder stage and put it into my Docker image.

docker build --no-cache . -t ghcr.io/saw303/zscsupporter-be/keycloak-18.0.0:0.0.1

Then I setup a Docker composition declaring a reverse proxy (Caddy), my Keycloak image and the MariaDB.

version: "3.9"
services:
  proxy:
    image: caddy:2.5.1-alpine
    ports:
      - "${PROXY_IP}:80:80"
      - "${PROXY_IP}:443:443"
    volumes:
      - ${BASE_PATH:-.}/docker-volume/caddy/Caddyfile:/etc/caddy/Caddyfile:Z
      - ${BASE_PATH:-.}/docker-volume/caddy/caddy_data:/data:Z
      - ${BASE_PATH:-.}/docker-volume/caddy/caddy_config:/config:Z
  
  keycloak:
    image: ghcr.io/saw303/zscsupporter-be/keycloak-18.0.0:0.0.1
    ports:
      - "127.0.0.1:9001:8080"
      - "127.0.0.1:9443:8443"
    environment:
      KC_HOSTNAME: localhost
      KC_HOSTNAME_PORT: 80
      KC_HOSTNAME_STRICT_BACKCHANNEL: true
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: admin
      KC_DB_URL: jdbc:mariadb://keycloakdb:3306/keycloak
      KC_DB_USERNAME: keycloak
      KC_DB_PASSWORD: secret
      KC_LOG_LEVEL: info
      KC_PROXY: edge

  keycloakdb:
    image: mariadb:10.7.3-focal
    environment:
      MYSQL_ROOT_PASSWORD: root_secret
      MYSQL_DATABASE: keycloak
      MYSQL_USER: keycloak
      MYSQL_PASSWORD: secret
      TZ: "Europe/Zurich"
    tmpfs:
      - /var/lib/mysql:rw
    ports:
      - "127.0.0.1:3307:3306"

My initial idea was to access Keycloak and its Admin Console using http (insecure), since I was running it on my local machine. The Caddyfile for Caddy Server 2.0 looks like this.

{
  admin off
}

localhost:80

reverse_proxy /* keycloak:8080

log

It simply passed all the request to the Keycloak container running on port 8080. But as you might have guessed. That did not work out very well. Clicking on the Admin Console link ended up in a blank browser page.

FFS, ...after some other failed attempts

I took my a while to understand that the Admin Console is requiring secure access by design. I did not find any way around it but finally found a why to make my reverse proxy creating self-signed certificates. So here is a working setup.

First of all, you need to configure Caddy to listen on e.g. port 443 and create a self-signed certificate by declaring tls internal.

{
  admin off
}

localhost:443 {
        reverse_proxy keycloak:8080
        tls internal
}

log

Then you need to build Keycloak a bit differently.

FROM quay.io/keycloak/keycloak:18.0.0 as builder

ENV KC_FEATURES=token-exchange
ENV KC_DB=mariadb
RUN /opt/keycloak/bin/kc.sh build

FROM quay.io/keycloak/keycloak:18.0.0
COPY --from=builder /opt/keycloak/ /opt/keycloak/
WORKDIR /opt/keycloak
ENTRYPOINT ["/opt/keycloak/bin/kc.sh", "start"]

And finally you got to adjust the Docker composition.

version: "3.9"
services:
  proxy:
    image: caddy:2.5.1-alpine
    ports:
      - "${PROXY_IP}:80:80"
      - "${PROXY_IP}:443:443"
    volumes:
      - ${BASE_PATH:-.}/docker-volume/caddy/Caddyfile:/etc/caddy/Caddyfile:Z
      - ${BASE_PATH:-.}/docker-volume/caddy/caddy_data:/data:Z
      - ${BASE_PATH:-.}/docker-volume/caddy/caddy_config:/config:Z

  keycloak:
    image: ghcr.io/saw303/zscsupporter-be/keycloak-18.0.0:0.0.1
    ports:
      - "127.0.0.1:9443:8443"
    restart: unless-stopped
    environment:
      KC_DB_URL: jdbc:mariadb://keycloakdb:3306/keycloak
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: admin
      KC_HOSTNAME: localhost
      KC_HOSTNAME_STRICT: false
      KC_HTTP_ENABLED: true
      KC_DB_USERNAME: keycloak
      KC_DB_PASSWORD: secret
      KC_PROXY: edge

  keycloakdb:
    image: mariadb:10.7.3-focal
    environment:
      MYSQL_ROOT_PASSWORD: root_secret
      MYSQL_DATABASE: keycloak
      MYSQL_USER: keycloak
      MYSQL_PASSWORD: secret
      TZ: "Europe/Zurich"
    tmpfs:
      - /var/lib/mysql:rw
    ports:
      - "127.0.0.1:3307:3306"

And this is how it worked for me. Have fun with your local Keycloak container on https://localhost.  Hope this helps.