Load Balancer with HAProxy + Keepalived
HAProxy/Keepalived Setup
- A cluster of two Load Balancer (LB) servers with HAProxy and Keepalived installed.
- Keepalived ensures high availability by switching from MASTER to BACKUP if the MASTER goes down or HAProxy fails.
- Two Web Servers (WS) running Apache.
In a real-world scenario, each LB should have two network interfaces:
- Internal – for communication with web servers
- External – for internet/public access
Web servers typically have one internal NIC only.
For training purposes, to simplify the setup, it’s recommended to use a single bridged adapter for all servers.
This allows:
- All servers to be on the same subnet
- Internet access from each server for package installation
Note: This is not a production-grade setup, but it’s ideal for learning and testing the HAProxy + Keepalived architecture.
For srtup instructions you can use the article Infrastructure 1 NIC.
Install Required Packages
Initial Setup on All 4 Servers, install EPEL repo:
sudo dnf install -y epel-release
On Load Balancers (LB1 & LB2). Install HAProxy and Keepalived:
sudo dnf install -y haproxy keepalived
On Web Servers (Web1 & Web2). Install and configure Apache:
sudo dnf install -y httpd
sudo systemctl enable --now httpd
echo "Hello from Web1" | sudo tee /var/www/html/index.html # on Web1
echo "Hello from Web2" | sudo tee /var/www/html/index.html # on Web2
Configuration
Configure HAProxy on Both LB1 and LB2. Edit /etc/haproxy/haproxy.cfg:
frontend http_front
bind 192.168.1.100:80
#You should paste your public IP or any free IP in youe local network for training purposes.
default_backend http_back
backend http_back
balance roundrobin
#Here you should paste your web servers IP’s.
server web1 192.168.1.21:80 check
server web2 192.168.1.22:80 check
It should look like this:

Check HAProxy configuration:
sudo haproxy -c -f /etc/haproxy/haproxy.cfg
# Expected output: Configuration file is valid
Configure Keepalived. Backup original config:
sudo cp /etc/keepalived/keepalived.conf etc/keepalived/keepalived.conf.back
LB1 (MASTER) Configuration. Clear and replace with:
global_defs {
script_user root
enable_script_security
}
vrrp_script chk_haproxy {
script "/usr/bin/pidof haproxy"
interval 2
timeout 5
fall 2
rise 1
}
vrrp_instance VI_1 {
state MASTER
interface enp0s3
virtual_router_id 51
priority 100
advert_int 1
authentication {
auth_type PASS
auth_pass 1234
}
virtual_ipaddress {
192.168.1.100
}
track_script {
chk_haproxy
}
notify_master "/etc/keepalived/on-master.sh"
notify_backup "/etc/keepalived/on-backup.sh"
}
LB2 (BACKUP) Configuration:
vrrp_instance VI_1 {
state BACKUP
interface enp0s3
virtual_router_id 51
priority 90
advert_int 1
authentication {
auth_type PASS
auth_pass 1234
}
virtual_ipaddress {
192.168.1.100
}
notify_master "/etc/keepalived/on-master.sh"
notify_backup "/etc/keepalived/on-backup.sh"
}
Note: Do not include track_script on the BACKUP node, as HAProxy is not active there unless it becomes MASTER.
Additional Reading
Detailed explanation of Keepalived configuration check here.
HAProxy + Keepalived for Ceph Object Gateway check here.
Keepalived allows you to execute scripts when its state changes:
Transition Script Triggered
→ MASTER notify_master
→ BACKUP notify_backup
→ FAULT notify_fault (optional)
Create the Transition Scripts on-master.sh:
sudo touch /etc/keepalived/on-master.sh
#Paste the following:
#!/bin/bash
systemctl start haproxy
on-backup.sh:
sudo touch /etc/keepalived/on-backup.sh
#Paste the following:
#!/bin/bash
systemctl stop haproxy
Make them executable:
sudo chmod +x /etc/keepalived/on-*.sh
FLOW OF WORK
Normal operation
- Keepalived starts on lb1
- It becomes MASTER (highest priority)
- Assigns VIP 192.168.1.100 to enp0s3
- Runs /etc/keepalived/on-master.sh → starts HAProxy
- Sends VRRP heartbeats every 1s to BACKUP
If HAProxy crashes
- chk_haproxy fails (e.g., pidof haproxy returns nothing)
- After 2 failures (fall 2), Keepalived marks node unhealthy
- Steps down → runs /etc/keepalived/on-backup.sh (stops HAProxy)
- VIP is removed
- BACKUP node sees no heartbeats → becomes MASTER
- BACKUP assigns VIP, starts HAProxy
If you include this track_script on the BACKUP node while HAProxy is stopped, it will always fail. The BACKUP node doesn’t need to track HAProxy because HAProxy only runs when the node becomes MASTER.
However, it does require the notify_master script so that, upon becoming MASTER, it can start HAProxy. It also needs the notify_backup script so that if it steps down, or if a server with a higher priority becomes active (such as the repaired original MASTER), it can stop HAProxy accordingly.
Firewall Configuration
On both LB1 and LB2:
sudo firewall-cmd --permanent --add-port=80/tcp
sudo firewall-cmd --zone=external --add-port=80/tcp --permanent
sudo firewall-cmd --permanent --add-rich-rule='rule protocol value="vrrp" accept'
sudo firewall-cmd –reload
On all servers:
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd –reload
On both LB1 and LB2 enable Non-local Bind to allow HAProxy to bind to the VIP before it’s assigned:
sudo vi /etc/sysctl.conf
# Add:
net.ipv4.ip_nonlocal_bind = 1
# Apply:
sudo sysctl -p
Start the Services
On LB1:
sudo systemctl enable --now keepalived
sudo systemctl enable --now haproxy
On LB2:
sudo systemctl enable --now keepalived
Verify the VIP on LB1:
ip a
You should see the VIP assigned to enp0s3. Something like this:

Testing
Open your browser and enter the VIP address.
Refresh the page several times — you should see the message alternate between “Hello from Web1” and “Hello from Web2”, indicating that HAProxy is successfully load balancing between the web servers.
Now, test failover by stopping Keepalived or HAProxy on LB1:
sudo systemctl stop haproxy
The VIP should automatically move to LB2, and HAProxy on LB2 should start working.
To switch back to LB1, simply start HAProxy again:
sudo systemctl start haproxy
Since LB1 has a higher priority, the VIP will float back to it automatically.