Keycloak Cluster with Docker Compose — Up and Running in Seconds
Follow up on my keycloak blogs about
- Running Keycloak in Docker with an external DB
- Run Keycloak locally with Docker compose
- backup and restore Keycloak
- Keycloak Cluster with Docker Compose — Up and Running in Seconds
In this blog, I’d like to show you how you can run Keycloak with cluster in seconds. This post shares the solutions to setup Keycloak cluster in docker compose on same host.
It already supports DNS name access, so the same solution should be suitable for cross-data center (DC), Docker cross-host, Kubernetes cluster, and other similar scenarios.
Understanding Keycloak Cluster Architecture
Keycloak with cluster is great for seamless authentication service with fault tolerance, load distribution, and scalability, it is High Availability (HA) solution for you.
Preparing the Environment
To simplify the setting, I made all in one docker compose file. You can easily take a reference and re-write for your own environment if you want, for example, to extend the solution to Kubernetes.
Make sure you have installed and enabled Docker service and install docker compose as well.
Due to the latest Vulnerability CVE-2024–21626 detected in Feb 2024, follow my another blog for detail (Illustrate runC Escape Vulnerability CVE-2024–21626 with my tests) , please update your docker to later version ASAP.
Three files you required for this experiment, copy and put them in same folder.
- Dockerfile
- nginx.conf
- docker-compose-cluster.yml
Dockerfile (Please go through Run Keycloak locally with Docker compose, I explained, why we can’t directly use the official docker image quay.io/keycloak/keycloak
)
# Documentation:
# https://www.keycloak.org/server/containers
ARG KEYCLOAK_VERSION
FROM quay.io/keycloak/keycloak:$KEYCLOAK_VERSION as builder
# Configure postgres database vendor
ENV KC_DB=postgres
ENV KC_FEATURES="token-exchange,scripts,preview"
WORKDIR /opt/keycloak
# If run the image in kubernetes, switch and active below line.
# RUN /opt/keycloak/bin/kc.sh build --cache=ispn --cache-stack=kubernetes --health-enabled=true --metrics-enabled=true
RUN /opt/keycloak/bin/kc.sh build --cache=ispn --health-enabled=true --metrics-enabled=true
FROM quay.io/keycloak/keycloak:$KEYCLOAK_VERSION
LABEL image.version=$KEYCLOAK_VERSION
COPY --from=builder /opt/keycloak/ /opt/keycloak/
# If any themes
# COPY themes/<nice-themes> /opt/keycloak/themes/<nice-themes>
# https://github.com/keycloak/keycloak/issues/19185#issuecomment-1480763024
USER root
RUN sed -i '/disabledAlgorithms/ s/ SHA1,//' /etc/crypto-policies/back-ends/java.config
USER keycloak
RUN /opt/keycloak/bin/kc.sh show-config
ENTRYPOINT ["/opt/keycloak/bin/kc.sh"]
nginx.conf (As the load balancer)
upstream backend {
server kc1:8080 fail_timeout=2s;
server kc2:8080 fail_timeout=2s;
}
server {
listen 8180;
server_name localhost;
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://backend;
proxy_connect_timeout 2s;
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
}
}
docker-compose-cluster.yml
# Make sure you have added "127.0.0.1 keycloak.com.au" into your local /etc/hosts file"
version: "3.9"
services:
postgres:
container_name: db
# for production, recommend postgres v15+
image: "postgres:15.5"
healthcheck:
test: [ "CMD", "pg_isready", "-q", "-d", "postgres", "-U", "root" ]
timeout: 45s
interval: 10s
retries: 10
volumes:
- postgres_data:/var/lib/postgresql/data
#- ./sql:/docker-entrypoint-initdb.d/:ro # turn it on, if you need run init DB
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: keycloak
POSTGRES_HOST: postgres
networks:
- local
ports:
- "5432:5432"
kc1:
container_name: kc1
build:
context: .
args:
# test well with 21.0.0 and 22.0.0
KEYCLOAK_VERSION: latest
command: ['start', '--optimized']
depends_on:
- "postgres"
environment:
JAVA_OPTS_APPEND: -Dkeycloak.profile.feature.upload_scripts=enabled
KC_DB_PASSWORD: postgres
KC_DB_URL: jdbc:postgresql://postgres/keycloak
KC_DB_USERNAME: postgres
KC_HEALTH_ENABLED: 'true'
KC_HTTP_ENABLED: 'true'
KC_METRICS_ENABLED: 'true'
# KC_HOSTNAME: keycloak.com.au
# KC_HOSTNAME_PORT: 8180
KC_HOSTNAME_URL: http://keycloak.com.au:8180
KC_PROXY: reencrypt
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: password
PROXY_ADDRESS_FORWARDING: "true"
CACHE_OWNERS_COUNT: 2
CACHE_OWNERS_AUTH_SESSIONS_COUNT: 2
JGROUPS_DISCOVERY_PROTOCOL: JDBC_PING
JGROUPS_DISCOVERY_EXTERNAL_IP: keycloak.com.au
JGROUPS_DISCOVERY_PROPERTIES: "datasource_jndi_name=java:jboss/datasources/KeycloakDS,initialize_sql=\"CREATE TABLE IF NOT EXISTS JGROUPSPING (own_addr varchar(200) NOT NULL, cluster_name varchar(200) NOT NULL, created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, ping_data BYTEA, constraint PK_JGROUPSPING PRIMARY KEY (own_addr, cluster_name))\",remove_all_data_on_view_change=true"
networks:
- local
kc2:
container_name: kc2
build:
context: .
args:
KEYCLOAK_VERSION: latest
command: ['start', '--optimized']
depends_on:
- "postgres"
environment:
JAVA_OPTS_APPEND: -Dkeycloak.profile.feature.upload_scripts=enabled
KC_DB_PASSWORD: postgres
KC_DB_URL: jdbc:postgresql://postgres/keycloak
KC_DB_USERNAME: postgres
KC_HEALTH_ENABLED: 'true'
KC_HTTP_ENABLED: 'true'
KC_METRICS_ENABLED: 'true'
# KC_HOSTNAME: keycloak.com.au
# KC_HOSTNAME_PORT: 8180
KC_HOSTNAME_URL: http://keycloak.com.au:8180
KC_PROXY: reencrypt
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: password
PROXY_ADDRESS_FORWARDING: "true"
CACHE_OWNERS_COUNT: 2
CACHE_OWNERS_AUTH_SESSIONS_COUNT: 2
JGROUPS_DISCOVERY_PROTOCOL: JDBC_PING
JGROUPS_DISCOVERY_EXTERNAL_IP: keycloak.com.au
JGROUPS_DISCOVERY_PROPERTIES: "datasource_jndi_name=java:jboss/datasources/KeycloakDS,initialize_sql=\"CREATE TABLE IF NOT EXISTS JGROUPSPING (own_addr varchar(200) NOT NULL, cluster_name varchar(200) NOT NULL, created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, ping_data BYTEA, constraint PK_JGROUPSPING PRIMARY KEY (own_addr, cluster_name))\",remove_all_data_on_view_change=true"
networks:
- local
lb:
container_name: kc_lb
image: nginx:alpine
volumes:
- ${PWD}/nginx.conf:/etc/nginx/conf.d/default.conf
ports:
- "8180:8180"
depends_on:
- kc1
- kc2
networks:
- local
networks:
local:
name: local
driver: bridge
volumes:
postgres_data:
Explanation
- JGROUPS_DISCOVERY_PROTOCOL — JGroups configuration. Its usage has been explained in Keycloak and JDBC Ping
- JGROUPS_DISCOVERY_EXTERNAL_IP — to make sure you can run with external DNS name, not localhost only
- CACHE_OWNERS_COUNT and CACHE_OWNERS_AUTH_SESSIONS_COUNT —The number of owners in Infinispan means “how many copies of data” will you have in your cluster. Whenever a node is added to cluster (or removed), Infinispan rebalances the cluster. If you scale down your cluster quickly, you may lose some data. In that case, Keycloak users will have to re-authenticate.
- JGROUPS_DISCOVERY_PROPERTIES — Another JGroups configuration, just copy it :-)
- PROXY_ADDRESS_FORWARDING — for cluster, you need enable it to true
Save, and put them under same folder
Let’s start the service
Step 1
update /etc/hosts
, add below lines
# keycloak
127.0.0.1 keycloak.com.au
On Windows, the file path is usually: c:\Windows\System32\Drivers\etc\hosts
In many online documents and videos, Keycloak experts often recommend starting the Keycloak service on localhost with a specific port. However, this practice is not advisable, especially when working in a real environment. Instead, it’s more practical to configure Keycloak with a DNS-ready setup. This also allows you to test HTTPS access with SSL certifications later on if needed.
Step 2
docker compose -f docker-compose-cluster.yml up -d
- Check the health
docker ps -a
- Check logs with Cluster events
docker logs -f <kc1 or kc2 container id>
- Check the cluster logs, there should be two members in cluster pool now
Received **new cluster view for channel** ISPN: [b31f28d4c94a-31765|1] (2) [b31f28d4c94a-31765, bc873530c08b-24274] Starting rebalance with members [b31f28d4c94a-31765, bc873530c08b-24274]
Step 3
access http://keycloak.com.au:8180
go with Administration Console, then login with admin / password
Step 4
Test fail over and cluster realiable.
- kill one keycloak container
docker ps -a
docker rm -f keycloak-compose-kc2
Check logs, you will only see one member in Cluster pool now.
> Updating cache members list [b31f28d4c94a-31765], topology id 6
When you refresh the website http://keycloak.com.au:8180, it takes about 5~10 seconds at first time, then work as normal
Step 5
Restore all services
$ docker compose -f docker-compose-cluster.yml up -d
✔ Container db Running
✔ Container kc1 Started # because I killed it before
✔ Container kc2 Running
✔ Container kc_lb Running
Check logs again, two members in cluster pool now.
> Starting rebalance with members [b31f28d4c94a-31765, 462ae7fcf1a3–41736], phase READ_OLD_WRITE_ALL, topology id 7 > Finished rebalance with members [b31f28d4c94a-31765, 462ae7fcf1a3–41736], topology id 10
If you access http://keycloak.com.au:8180, it still works fine
Conclusion
By following these step-by-step instructions and utilizing the provided code snippets, you can successfully deploy and maintain a Keycloak cluster in seconds. With less adjustment, you can meet the authentication needs of your organization. Embrace the power of clustering to enhance the availability, scalability, and security of your authentication infrastructure.
Codes, please
Yes, I have put these codes together at here
Reference
Keycloak Cluster Setup by Keycloak.org
KEYCLOAK Cluster — Up and Running in Seconds | Niko Köbler (@dasniko)
Learning is fun
# keycloak # Security # Cluster # Keyclaok cluster # container # Docker # Docker compose # solution # DevOps