Set up a production-ready Keycloak cluster with PostgreSQL backend, HAProxy load balancing, and automatic failover to ensure identity services remain available during node failures and high traffic.
Prerequisites
- At least 7 servers (3 Keycloak, 2 PostgreSQL, 2 HAProxy)
- Static IP addresses for all nodes
- SSL certificates for HTTPS
- Basic understanding of PostgreSQL administration
- Root access on all servers
What this solves
Single Keycloak instances create identity bottlenecks and fail during peak authentication loads or server outages. This tutorial builds a production-ready Keycloak cluster with shared PostgreSQL storage, HAProxy load balancing, and automatic failover to handle thousands of concurrent users across multiple nodes.
Prerequisites and cluster planning
Infrastructure requirements
Plan your cluster topology before installation. You need at least 3 servers for a minimal production setup.
| Component | Minimum specs | Quantity | Purpose |
|---|---|---|---|
| HAProxy nodes | 2 CPU, 4GB RAM | 2 | Load balancing with redundancy |
| Keycloak nodes | 4 CPU, 8GB RAM | 3+ | Identity provider cluster |
| PostgreSQL nodes | 4 CPU, 16GB RAM | 2 | Database cluster with replication |
Network configuration
Configure static IP addresses and hostname resolution across all cluster nodes.
# Add to all cluster nodes
203.0.113.10 haproxy1.example.com haproxy1
203.0.113.11 haproxy2.example.com haproxy2
203.0.113.20 keycloak1.example.com keycloak1
203.0.113.21 keycloak2.example.com keycloak2
203.0.113.22 keycloak3.example.com keycloak3
203.0.113.30 postgres1.example.com postgres1
203.0.113.31 postgres2.example.com postgres2
Firewall rules for cluster communication
Open required ports between cluster nodes for internal communication.
sudo ufw allow from 203.0.113.0/24 to any port 8080 comment "Keycloak HTTP"
sudo ufw allow from 203.0.113.0/24 to any port 7800 comment "Keycloak clustering"
sudo ufw allow from 203.0.113.0/24 to any port 5432 comment "PostgreSQL"
sudo ufw allow from 203.0.113.0/24 to any port 8080 comment "HAProxy stats"
sudo ufw reload
Configure PostgreSQL database cluster
Install PostgreSQL on primary node
Set up the primary PostgreSQL instance that will replicate to secondary nodes.
sudo apt update
sudo apt install -y postgresql postgresql-contrib
sudo systemctl enable --now postgresql
Create Keycloak database and user
Set up the dedicated database and credentials for Keycloak cluster access.
sudo -u postgres psql
CREATE DATABASE keycloak;
CREATE USER keycloak_user WITH ENCRYPTED PASSWORD 'kc_cluster_pass_2024!';
GRANT ALL PRIVILEGES ON DATABASE keycloak TO keycloak_user;
GRANT ALL ON SCHEMA public TO keycloak_user;
\q
Configure PostgreSQL for clustering
Enable streaming replication and configure connection limits for the Keycloak cluster.
# Connection settings
max_connections = 200
shared_buffers = 256MB
effective_cache_size = 1GB
Replication settings
wal_level = replica
max_wal_senders = 3
max_replication_slots = 3
archive_mode = on
archive_command = 'cp %p /var/lib/postgresql/16/main/archive/%f'
Network settings
listen_addresses = '*'
port = 5432
Configure client authentication
Set up access rules for Keycloak nodes and replication users.
# Local connections
local all postgres peer
local all all peer
Keycloak cluster connections
host keycloak keycloak_user 203.0.113.0/24 scram-sha-256
Replication connections
host replication replicator 203.0.113.31/32 scram-sha-256
Local network
host all all 127.0.0.1/32 scram-sha-256
host all all ::1/128 scram-sha-256
Create replication user and restart
Add the replication user and apply configuration changes.
sudo -u postgres createuser --replication --pwprompt replicator
sudo systemctl restart postgresql
sudo systemctl status postgresql
Set up Keycloak cluster nodes
Install Java and download Keycloak
Install the required Java runtime and download Keycloak on all cluster nodes.
sudo apt update
sudo apt install -y openjdk-21-jdk wget
sudo useradd -r -s /bin/false keycloak
sudo mkdir -p /opt/keycloak
Download and extract Keycloak
Get the latest Keycloak release and set up the directory structure.
cd /tmp
wget https://github.com/keycloak/keycloak/releases/download/22.0.5/keycloak-22.0.5.tar.gz
sudo tar -xzf keycloak-22.0.5.tar.gz -C /opt/keycloak --strip-components=1
sudo chown -R keycloak:keycloak /opt/keycloak
sudo chmod +x /opt/keycloak/bin/kc.sh
Configure database connection
Set up PostgreSQL connection parameters for the cluster database.
# Database configuration
db=postgres
db-url=jdbc:postgresql://postgres1.example.com:5432/keycloak
db-username=keycloak_user
db-password=kc_cluster_pass_2024!
Cluster configuration
cache=ispn
cache-stack=tcp
HTTP configuration
http-host=0.0.0.0
http-port=8080
Hostname configuration
hostname=keycloak.example.com
hostname-strict=false
hostname-strict-https=false
Proxy configuration
proxy=edge
Health and metrics
health-enabled=true
metrics-enabled=true
Configure cluster discovery
Set up JGroups configuration for automatic node discovery and communication.
Build optimized Keycloak
Build Keycloak with database and cluster optimizations for production.
sudo -u keycloak /opt/keycloak/bin/kc.sh build --db=postgres --cache=ispn
Create systemd service
Set up systemd service for automatic startup and process management.
[Unit]
Description=Keycloak Identity Provider
After=network.target
[Service]
Type=notify
User=keycloak
Group=keycloak
ExecStart=/opt/keycloak/bin/kc.sh start
TimeoutStartSec=600
TimeoutStopSec=30
RestartSec=5
Restart=always
StandardOutput=journal
StandardError=journal
SyslogIdentifier=keycloak
Environment="JAVA_OPTS=-Xms2g -Xmx4g -XX:MetaspaceSize=256m"
Environment="KEYCLOAK_ADMIN=admin"
Environment="KEYCLOAK_ADMIN_PASSWORD=admin_cluster_2024!"
[Install]
WantedBy=multi-user.target
Start Keycloak cluster nodes
Enable and start Keycloak on each cluster node in sequence.
sudo systemctl daemon-reload
sudo systemctl enable keycloak
sudo systemctl start keycloak
sudo systemctl status keycloak
Implement load balancing with HAProxy
Install HAProxy
Install HAProxy on both load balancer nodes for redundancy.
sudo apt update
sudo apt install -y haproxy keepalived
Configure HAProxy for Keycloak cluster
Set up load balancing with health checks and session affinity for Keycloak nodes.
global
daemon
maxconn 4096
log stdout local0
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin
stats timeout 30s
user haproxy
group haproxy
defaults
mode http
timeout connect 5000ms
timeout client 50000ms
timeout server 50000ms
option httplog
option dontlognull
option redispatch
retries 3
maxconn 2000
frontend keycloak_frontend
bind *:80
bind *:443 ssl crt /etc/ssl/certs/keycloak.pem
redirect scheme https if !{ ssl_fc }
# Security headers
http-response set-header X-Frame-Options DENY
http-response set-header X-Content-Type-Options nosniff
http-response set-header X-XSS-Protection "1; mode=block"
http-response set-header Strict-Transport-Security "max-age=31536000; includeSubDomains"
default_backend keycloak_backend
backend keycloak_backend
balance roundrobin
option httpchk GET /health/ready
http-check expect status 200
# Session affinity using cookies
cookie KEYCLOAK_SERVER insert indirect nocache
server keycloak1 keycloak1.example.com:8080 check cookie kc1 maxconn 500
server keycloak2 keycloak2.example.com:8080 check cookie kc2 maxconn 500
server keycloak3 keycloak3.example.com:8080 check cookie kc3 maxconn 500
listen stats
bind *:8080
stats enable
stats uri /stats
stats refresh 30s
stats admin if TRUE
Configure keepalived for HAProxy failover
Set up virtual IP failover between HAProxy nodes using VRRP.
# Primary HAProxy node (haproxy1)
vrrp_script chk_haproxy {
script "/bin/curl -f http://localhost:8080/stats || exit 1"
interval 2
weight -2
fall 3
rise 2
}
vrrp_instance VI_1 {
state MASTER
interface eth0
virtual_router_id 51
priority 110
advert_int 1
authentication {
auth_type PASS
auth_pass kc_vrrp_2024
}
virtual_ipaddress {
203.0.113.100/24
}
track_script {
chk_haproxy
}
}
Start load balancing services
Enable and start HAProxy and keepalived on both load balancer nodes.
sudo systemctl enable --now haproxy
sudo systemctl enable --now keepalived
sudo systemctl status haproxy
sudo systemctl status keepalived
Configure session replication and monitoring
Verify cluster formation
Check that Keycloak nodes have discovered each other and formed a cluster.
sudo journalctl -u keycloak -f | grep -i cluster
curl -s http://keycloak1.example.com:8080/health/ready
curl -s http://keycloak2.example.com:8080/health/ready
curl -s http://keycloak3.example.com:8080/health/ready
Test session replication
Verify that user sessions are shared across cluster nodes.
# Access admin console through load balancer
curl -I http://203.0.113.100/admin/
Check session distribution
curl -s http://keycloak1.example.com:8080/admin/realms/master/sessions
Configure cluster monitoring
Set up monitoring endpoints for cluster health and performance metrics. This integrates with existing HAProxy and Consul monitoring setups.
# Add monitoring configuration
metrics-enabled=true
health-enabled=true
JVM metrics
jvm-metrics-enabled=true
Cache metrics
cache-metrics-enabled=true
Test failover scenarios
Test Keycloak node failover
Simulate node failures to verify automatic failover and session continuity.
# Stop one Keycloak node
sudo systemctl stop keycloak
Test application access continues
curl -I http://203.0.113.100/admin/
Check HAProxy stats
curl -s http://203.0.113.100:8080/stats
Test HAProxy failover
Test virtual IP failover between HAProxy nodes.
# Stop primary HAProxy
sudo systemctl stop haproxy
Verify VIP moves to backup
ping 203.0.113.100
Test continued access
curl -I http://203.0.113.100/admin/
Test database failover
Verify application resilience during database maintenance windows.
# Promote PostgreSQL replica (on postgres2)
sudo -u postgres pg_ctl promote -D /var/lib/postgresql/16/main/
Update Keycloak database configuration
Restart Keycloak nodes with new primary database
Verify your setup
# Check all services are running
sudo systemctl status postgresql haproxy keepalived keycloak
Verify cluster health
curl -s http://203.0.113.100/health/ready
curl -s http://203.0.113.100/health/live
Check HAProxy statistics
curl -s http://203.0.113.100:8080/stats
Verify admin console access
curl -I http://203.0.113.100/admin/
Check cluster member communication
sudo ss -tulpn | grep :7800
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Nodes not joining cluster | Firewall blocking port 7800 | Open clustering ports: sudo ufw allow 7800 |
| Database connection errors | PostgreSQL authentication failure | Check pg_hba.conf and restart PostgreSQL |
| Sessions not replicating | Cache configuration mismatch | Verify cache-ispn.xml is identical on all nodes |
| HAProxy health checks failing | Keycloak health endpoint not responding | Check health-enabled=true in keycloak.conf |
| VIP failover not working | Keepalived authentication mismatch | Verify matching auth_pass in keepalived.conf |
| High memory usage | JVM heap size misconfigured | Tune JAVA_OPTS: -Xms4g -Xmx8g based on available RAM |
Next steps
- Configure OAuth2 integration with your applications
- Set up SAML integration for enterprise SSO
- Configure custom themes and branding for your organization
- Set up monitoring with Prometheus and Grafana
- Implement backup automation and disaster recovery
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# Global variables
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOG_FILE="/var/log/keycloak-cluster-install.log"
# Default configuration
CLUSTER_SUBNET="${CLUSTER_SUBNET:-203.0.113.0/24}"
NODE_TYPE="${1:-}"
NODE_IP="${2:-}"
KC_DB_PASSWORD="${KC_DB_PASSWORD:-$(openssl rand -base64 32)}"
usage() {
echo "Usage: $0 <node_type> <node_ip> [cluster_subnet]"
echo "Node types: haproxy, keycloak, postgres-primary, postgres-replica"
echo "Example: $0 keycloak 203.0.113.20 203.0.113.0/24"
exit 1
}
log() {
echo -e "${GREEN}[$(date '+%Y-%m-%d %H:%M:%S')]${NC} $1" | tee -a "$LOG_FILE"
}
warn() {
echo -e "${YELLOW}[WARNING]${NC} $1" | tee -a "$LOG_FILE"
}
error() {
echo -e "${RED}[ERROR]${NC} $1" | tee -a "$LOG_FILE"
exit 1
}
cleanup() {
if [[ $? -ne 0 ]]; then
error "Installation failed. Check $LOG_FILE for details."
fi
}
trap cleanup ERR
detect_distro() {
if [[ ! -f /etc/os-release ]]; then
error "/etc/os-release not found. Cannot detect distribution."
fi
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_INSTALL="apt install -y"
PKG_UPDATE="apt update"
FIREWALL_CMD="ufw"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_INSTALL="dnf install -y"
PKG_UPDATE="dnf update -y"
FIREWALL_CMD="firewalld"
;;
amzn)
PKG_MGR="yum"
PKG_INSTALL="yum install -y"
PKG_UPDATE="yum update -y"
FIREWALL_CMD="firewalld"
;;
*)
error "Unsupported distribution: $ID"
;;
esac
}
check_prerequisites() {
log "[1/12] Checking prerequisites..."
if [[ $EUID -ne 0 ]]; then
error "This script must be run as root"
fi
if [[ -z "$NODE_TYPE" || -z "$NODE_IP" ]]; then
usage
fi
if ! command -v openssl &> /dev/null; then
$PKG_INSTALL openssl
fi
}
configure_hosts() {
log "[2/12] Configuring hosts file..."
cat >> /etc/hosts << EOF
# Keycloak Cluster Configuration
203.0.113.10 haproxy1.example.com haproxy1
203.0.113.11 haproxy2.example.com haproxy2
203.0.113.20 keycloak1.example.com keycloak1
203.0.113.21 keycloak2.example.com keycloak2
203.0.113.22 keycloak3.example.com keycloak3
203.0.113.30 postgres1.example.com postgres1
203.0.113.31 postgres2.example.com postgres2
EOF
}
configure_firewall() {
log "[3/12] Configuring firewall..."
case "$FIREWALL_CMD" in
ufw)
systemctl enable --now ufw
ufw --force enable
ufw allow from $CLUSTER_SUBNET to any port 8080 comment "Keycloak HTTP"
ufw allow from $CLUSTER_SUBNET to any port 7800 comment "Keycloak clustering"
ufw allow from $CLUSTER_SUBNET to any port 5432 comment "PostgreSQL"
ufw allow from $CLUSTER_SUBNET to any port 8404 comment "HAProxy stats"
ufw reload
;;
firewalld)
systemctl enable --now firewalld
firewall-cmd --permanent --add-rich-rule="rule family=ipv4 source address=$CLUSTER_SUBNET port protocol=tcp port=8080 accept"
firewall-cmd --permanent --add-rich-rule="rule family=ipv4 source address=$CLUSTER_SUBNET port protocol=tcp port=7800 accept"
firewall-cmd --permanent --add-rich-rule="rule family=ipv4 source address=$CLUSTER_SUBNET port protocol=tcp port=5432 accept"
firewall-cmd --permanent --add-rich-rule="rule family=ipv4 source address=$CLUSTER_SUBNET port protocol=tcp port=8404 accept"
firewall-cmd --reload
;;
esac
}
install_java() {
log "[4/12] Installing Java..."
case "$PKG_MGR" in
apt)
$PKG_INSTALL openjdk-17-jdk
;;
dnf|yum)
$PKG_INSTALL java-17-openjdk-devel
;;
esac
}
install_keycloak() {
log "[5/12] Installing Keycloak..."
KC_VERSION="23.0.3"
cd /opt
wget -q "https://github.com/keycloak/keycloak/releases/download/$KC_VERSION/keycloak-$KC_VERSION.tar.gz"
tar -xzf "keycloak-$KC_VERSION.tar.gz"
mv "keycloak-$KC_VERSION" keycloak
chown -R root:root /opt/keycloak
chmod -R 755 /opt/keycloak
useradd -r -s /bin/false keycloak || true
chown -R keycloak:keycloak /opt/keycloak
}
configure_keycloak() {
log "[6/12] Configuring Keycloak cluster..."
mkdir -p /opt/keycloak/conf
cat > /opt/keycloak/conf/keycloak.conf << EOF
# Database configuration
db=postgres
db-url=jdbc:postgresql://postgres1.example.com:5432/keycloak
db-username=keycloak_user
db-password=$KC_DB_PASSWORD
# Cluster configuration
cache=ispn
cache-config-file=cluster-ispn.xml
# Network configuration
hostname=$NODE_IP
http-enabled=true
http-port=8080
# Clustering
jgroups-bind-addr=$NODE_IP
jgroups-bind-port=7800
EOF
cat > /opt/keycloak/conf/cluster-ispn.xml << 'EOF'
<infinispan xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="urn:infinispan:config:13.0 http://www.infinispan.org/schemas/infinispan-config-13.0.xsd"
xmlns="urn:infinispan:config:13.0">
<jgroups>
<stack name="tcp" extends="tcp">
<TCP bind_addr="${jgroups.bind.address:127.0.0.1}" bind_port="${jgroups.bind.port:7800}"/>
<TCPPING initial_hosts="keycloak1[7800],keycloak2[7800],keycloak3[7800]" port_range="0"/>
</stack>
</jgroups>
<cache-container name="keycloak">
<transport cluster="${keycloak.cache.cluster.name:keycloak}" stack="tcp"/>
<local-cache name="realms"/>
<local-cache name="users"/>
<distributed-cache name="sessions"/>
<distributed-cache name="authenticationSessions"/>
<distributed-cache name="offlineSessions"/>
<distributed-cache name="clientSessions"/>
<distributed-cache name="offlineClientSessions"/>
<distributed-cache name="loginFailures"/>
<distributed-cache name="actionTokens"/>
</cache-container>
</infinispan>
EOF
chown keycloak:keycloak /opt/keycloak/conf/*
chmod 640 /opt/keycloak/conf/*
}
install_postgres() {
log "[7/12] Installing PostgreSQL..."
case "$PKG_MGR" in
apt)
$PKG_INSTALL postgresql postgresql-contrib
;;
dnf|yum)
$PKG_INSTALL postgresql-server postgresql-contrib
postgresql-setup --initdb
;;
esac
systemctl enable --now postgresql
}
configure_postgres_primary() {
log "[8/12] Configuring PostgreSQL primary..."
sudo -u postgres psql << EOF
CREATE DATABASE keycloak;
CREATE USER keycloak_user WITH ENCRYPTED PASSWORD '$KC_DB_PASSWORD';
GRANT ALL PRIVILEGES ON DATABASE keycloak TO keycloak_user;
CREATE USER replicator WITH REPLICATION ENCRYPTED PASSWORD 'repl_pass_2024!';
EOF
case "$PKG_MGR" in
apt)
PG_CONF="/etc/postgresql/*/main/postgresql.conf"
PG_HBA="/etc/postgresql/*/main/pg_hba.conf"
;;
dnf|yum)
PG_CONF="/var/lib/pgsql/data/postgresql.conf"
PG_HBA="/var/lib/pgsql/data/pg_hba.conf"
;;
esac
cp $PG_CONF $PG_CONF.backup
cat >> $PG_CONF << EOF
# Cluster configuration
max_connections = 200
shared_buffers = 256MB
effective_cache_size = 1GB
wal_level = replica
max_wal_senders = 3
max_replication_slots = 3
listen_addresses = '*'
EOF
cp $PG_HBA $PG_HBA.backup
cat >> $PG_HBA << EOF
host keycloak keycloak_user $CLUSTER_SUBNET scram-sha-256
host replication replicator 203.0.113.31/32 scram-sha-256
EOF
systemctl restart postgresql
}
install_haproxy() {
log "[9/12] Installing HAProxy..."
$PKG_INSTALL haproxy
cp /etc/haproxy/haproxy.cfg /etc/haproxy/haproxy.cfg.backup
cat > /etc/haproxy/haproxy.cfg << EOF
global
daemon
maxconn 4096
defaults
mode http
timeout connect 5000ms
timeout client 50000ms
timeout server 50000ms
frontend keycloak_frontend
bind *:80
default_backend keycloak_backend
backend keycloak_backend
balance roundrobin
option httpchk GET /auth/realms/master
server keycloak1 keycloak1:8080 check
server keycloak2 keycloak2:8080 check
server keycloak3 keycloak3:8080 check
listen stats
bind *:8404
stats enable
stats uri /stats
stats refresh 30s
EOF
systemctl enable --now haproxy
}
create_systemd_service() {
log "[10/12] Creating systemd service..."
if [[ "$NODE_TYPE" == "keycloak" ]]; then
cat > /etc/systemd/system/keycloak.service << EOF
[Unit]
Description=Keycloak Identity Provider
After=network.target postgresql.service
Wants=network.target
[Service]
Type=notify
User=keycloak
Group=keycloak
ExecStart=/opt/keycloak/bin/kc.sh start --optimized
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=keycloak
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable keycloak
fi
}
start_services() {
log "[11/12] Starting services..."
case "$NODE_TYPE" in
keycloak)
/opt/keycloak/bin/kc.sh build
systemctl start keycloak
;;
postgres-primary)
systemctl restart postgresql
;;
haproxy)
systemctl restart haproxy
;;
esac
}
verify_installation() {
log "[12/12] Verifying installation..."
case "$NODE_TYPE" in
keycloak)
if systemctl is-active --quiet keycloak; then
log "Keycloak service is running"
else
error "Keycloak service failed to start"
fi
;;
postgres-primary)
if systemctl is-active --quiet postgresql; then
log "PostgreSQL service is running"
else
error "PostgreSQL service failed to start"
fi
;;
haproxy)
if systemctl is-active --quiet haproxy; then
log "HAProxy service is running"
else
error "HAProxy service failed to start"
fi
;;
esac
}
main() {
detect_distro
check_prerequisites
configure_hosts
configure_firewall
$PKG_UPDATE
case "$NODE_TYPE" in
keycloak)
install_java
install_keycloak
configure_keycloak
create_systemd_service
start_services
;;
postgres-primary)
install_postgres
configure_postgres_primary
start_services
;;
haproxy)
install_haproxy
start_services
;;
*)
error "Invalid node type: $NODE_TYPE"
;;
esac
verify_installation
log "Installation completed successfully!"
log "Database password: $KC_DB_PASSWORD"
log "Log file: $LOG_FILE"
}
main "$@"
Review the script before running. Execute with: bash install.sh