Keycloak | Cluster | Production

Keycloak, the popular open-source identity and access management solution, has introduced a significant change in its 26.1.0 release. The default transport stack for cluster discovery has been switched to JDBC-PING, simplifying cluster setup and improving cloud compatibility.

This article explores this new feature and demonstrates how to set up a local three-node Keycloak cluster for production using Podman Compose, with Keycloak configured to use HTTPS. We will also look at a managed alternative for running Keycloak in the cloud.

🚀 We will also look at a managed alternative for running Keycloak in the cloud.

Note: I've been maintaining a project called Keycloak Clustered for years. This project adds JDBC-PING discovery protocol to the official Keycloak Docker image. Well, it seems I don't need to maintain it anymore as Keycloak now includes JDBC-PING discovery by default.

Understanding JDBC-PING

What is JDBC-PING?

JDBC-PING is a discovery protocol that allows Keycloak nodes to find each other using a shared database. This method replaces the previous default of UDP multicast, which often required complex network configurations, especially in cloud environments.

Benefits of JDBC-PING

  1. Simplified configuration
  2. Improved cloud compatibility
  3. No need for additional network-related setups
  4. Works out-of-the-box in most environments

Prerequisites

To follow along with this guide, please ensure that you have some containerization tool (Docker, Podman, etc.) installed on your machine.

Create Project Folders

Create a folder named keycloak-cluster in your workspace. Inside it, let's create a subfolder called certificates.

Setting Up Keycloak to Use HTTPS

When an application communicates with Keycloak, it's crucial that this communication is secure because sensitive information is being exchanged. In production, we should never expose Keycloak endpoints through HTTP. Using Transport Layer Security (TLS) ensures secure and encrypted data exchanges.

We will use OpenSSL to generate both the private key file and the certificate PEM file.

Generate a Private Key

In a terminal, make sure you are in the keycloak-cluster folder. Then, run the following command to generate a 2048-bit RSA private key:

openssl genrsa -out certificates/private.key 2048

This will create a file named private.key inside the certificates subfolder.

Generate a Self-Signed Certificate

In a terminal and still inside the keycloak-cluster folder, let's create a self-signed certificate valid for 365 days:

openssl req -x509 -new \
  -key certificates/private.key \
  -out certificates/certificate.pem \
  -days 365

We will be prompted to enter information such as Country Name, State or Province Name, etc. Fill in the fields accordingly.

You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:
Email Address []:

This will produce a file named certificate.pem inside the certificates subfolder.

Prepare the Podman Compose File

In the keycloak-cluster folder, create the compose.yml file with the following content:

services:

  postgres:
    image: 'postgres:18.4'
    container_name: 'postgres'
    restart: 'unless-stopped'
    ports:
      - '5432:5432'
    environment:
      - 'POSTGRES_DB=keycloak'
      - 'POSTGRES_USER=keycloak'
      - 'POSTGRES_PASSWORD=password'

  keycloak-1:
    image: 'quay.io/keycloak/keycloak:26.7.3'
    container_name: 'keycloak-1'
    restart: 'unless-stopped'
    environment:
      - 'KC_BOOTSTRAP_ADMIN_USERNAME=admin'
      - 'KC_BOOTSTRAP_ADMIN_PASSWORD=admin'
      - 'KC_DB=postgres'
      - 'KC_DB_URL_HOST=postgres'
      - 'KC_DB_URL_DATABASE=keycloak'
      - 'KC_DB_USERNAME=keycloak'
      - 'KC_DB_PASSWORD=password'
      - 'KC_HOSTNAME=localhost'
      - 'KC_CACHE=ispn'
      - 'KC_LOG_LEVEL=INFO,org.jgroups:DEBUG'
    ports:
      - '8443:8443'
    command: 'start --https-certificate-file=/etc/x509/https/certificate.pem --https-certificate-key-file=/etc/x509/https/private.key'
    depends_on:
      - 'postgres'
    volumes:
      - './certificates:/etc/x509/https'

  keycloak-2:
    image: 'quay.io/keycloak/keycloak:26.7.3'
    container_name: 'keycloak-2'
    restart: 'unless-stopped'
    environment:
      - 'KC_BOOTSTRAP_ADMIN_USERNAME=admin'
      - 'KC_BOOTSTRAP_ADMIN_PASSWORD=admin'
      - 'KC_DB=postgres'
      - 'KC_DB_URL_HOST=postgres'
      - 'KC_DB_URL_DATABASE=keycloak'
      - 'KC_DB_USERNAME=keycloak'
      - 'KC_DB_PASSWORD=password'
      - 'KC_HOSTNAME=localhost'
      - 'KC_CACHE=ispn'
      - 'KC_LOG_LEVEL=INFO,org.jgroups:DEBUG'
    ports:
      - '8444:8443'
    command: 'start --https-certificate-file=/etc/x509/https/certificate.pem --https-certificate-key-file=/etc/x509/https/private.key'
    depends_on:
      - 'postgres'
    volumes:
      - './certificates:/etc/x509/https'

  keycloak-3:
    image: 'quay.io/keycloak/keycloak:26.7.3'
    container_name: 'keycloak-3'
    restart: 'unless-stopped'
    environment:
      - 'KC_BOOTSTRAP_ADMIN_USERNAME=admin'
      - 'KC_BOOTSTRAP_ADMIN_PASSWORD=admin'
      - 'KC_DB=postgres'
      - 'KC_DB_URL_HOST=postgres'
      - 'KC_DB_URL_DATABASE=keycloak'
      - 'KC_DB_USERNAME=keycloak'
      - 'KC_DB_PASSWORD=password'
      - 'KC_HOSTNAME=localhost'
      - 'KC_CACHE=ispn'
      - 'KC_LOG_LEVEL=INFO,org.jgroups:DEBUG'
    ports:
      - '8445:8443'
    command: 'start --https-certificate-file=/etc/x509/https/certificate.pem --https-certificate-key-file=/etc/x509/https/private.key'
    depends_on:
      - 'postgres'
    volumes:
      - './certificates:/etc/x509/https'

This configuration sets up three Keycloak nodes and a PostgreSQL database. We are using the start command, which is the appropriate command for production. We are also specifying the certificates created in the previous section. Additionally, we set KC_CACHE=ispn to enable shared Infinispan caches across the cluster nodes, ensuring that sessions and realms are synchronized.

Note: We use KC_HOSTNAME=localhost since this is a local setup. In a real production environment, this should be set to the actual hostname of your Keycloak server.

Note: We set org.jgroups to DEBUG log level to better observe the cluster discovery process. In a real production environment, you would use INFO or WARN to avoid excessive logging.

Start the Cluster

Run the following command in the directory containing your compose.yml file:

podman compose up -d

This command will start your Keycloak cluster in detached mode.

Verify the Cluster

To confirm that the Keycloak cluster has formed, we can check two things: the logs and the PostgreSQL database. We look at the database because Keycloak is using JDBC-PING for cluster discovery, which stores cluster information in the PostgreSQL database.

Checking the logs

In a terminal, run the following command:

podman logs keycloak-1

In the logs, we can see the following line:

INFO  [org.infinispan.LIFECYCLE] () [Context=clientSessions] ISPN100010: Finished rebalance with members [54986d1a920a-53296, 5999a1b5ee7d-23884, ad16783f49a9-24001], topology id 8

This indicates that the cluster has formed and has three members.

Checking the PostgreSQL database

Let's access the PostgreSQL shell. In a terminal, run the following command:

podman exec -it postgres psql -U keycloak -d keycloak

Once in the shell, run:

\dt jgroups_ping

It should return:

              List of tables
 Schema |     Name     | Type  |  Owner
--------+--------------+-------+----------
 public | jgroups_ping | table | keycloak
(1 row)

Let's select the records in the jgroups_ping table. To do so, run:

select * from jgroups_ping;

We should see something like:

                   address                   |        name        | cluster_name |       ip       | coord | last_update |               coordinated_by
---------------------------------------------+--------------------+--------------+----------------+-------+-------------+---------------------------------------------
 uuid://00000000-0000-0000-0000-000000000001 | 34bbdd559d79-8865  | ISPN         | 10.89.0.3:7800 | t     |  1790233585 | uuid://00000000-0000-0000-0000-000000000001
 uuid://00000000-0000-0000-0000-000000000002 | e097b57888ed-13674 | ISPN         | 10.89.0.4:7800 | f     |  1790233585 | uuid://00000000-0000-0000-0000-000000000001
 uuid://00000000-0000-0000-0000-000000000003 | 7e710b136482-2980  | ISPN         | 10.89.0.6:7800 | f     |  1790233585 | uuid://00000000-0000-0000-0000-000000000001
(3 rows)

To exit the PostgreSQL shell type exit.

Demonstration

Checking Admin Sessions

  • Open three separate browsers (e.g., Chrome, Safari, and Firefox) or use different browser profiles (e.g., Chrome, Incognito Chrome, and Firefox).
  • In one browser, access https://localhost:8443; in another browser, access https://localhost:8444; and in the last browser, access https://localhost:8445.
  • Use "admin" as both the username and password to log in.
  • In one of the browsers, click "Sessions" on the left menu.
  • Observe that the "admin" user has three active sessions.
None

Creating a new realm

  • In one of the browsers, let's create a new realm named my-realm.
  • After creating the realm, switch to the other browsers and refresh their pages.
  • The new realm should now be visible in all browsers.

Shutdown

Make sure you are in the keycloak-cluster folder and execute the following command:

podman compose down -v

Skycloak: Managed Keycloak in the Cloud

While this article demonstrated a local production-like cluster, running Keycloak in production on the cloud requires handling upgrades, CVE patching, high availability, backups, and monitoring. If you'd rather focus on your product instead of maintaining auth infrastructure, consider Skycloak.

None

Skycloak is a managed identity platform built on real upstream Keycloak. It offers:

  • 99.99% uptime SLA with multi-site deployments
  • Automatic updates and CVE patching
  • Multi-region data residency (US, EU, Canada, Australia, and more)
  • Unlimited users with no per-MAU tax
  • SOC 2 Type II, ISO 27001, GDPR, and HIPAA compliance
  • Full Keycloak admin access with export and self-host anytime
  • Full managed Keycloak cluster trial: 7 days, no card.
  • Connect-an-app / realm onboarding trial: 21 days.

Conclusion

Keycloak's switch to JDBC-PING in version 26.7.3 makes setting up clusters much easier. This article showed how to use this feature to create a local Keycloak cluster for production with three nodes, using Podman Compose and HTTPS. This change helps make Keycloak more flexible and user-friendly for managing identities across different environments.

Additional Readings

Thanks for Reading

If you found this article useful, here are a few ways you can support my work:

  • 🔁 Repost.
  • 👏 Clap, highlight, and respond.
  • ✉️ Subscribe to my newsletter.
  • 🔔 Follow me on Medium | LinkedIn | X | GitHub.
  • ☕ Support my writing
None