Putting a load balancer in front of your servers removes one single point of failure, and creates another: the load balancer itself. keepalived solves that with VRRP (Virtual Router Redundancy Protocol). Two servers share a virtual IP (VIP). The active one holds it, and if it fails, the standby takes over the IP within a second or two, so clients keep connecting to the same address.
How it works
VIP 10.0.0.100 (clients connect here)
│
┌──────────────┴──────────────┐
lb1 (MASTER, prio 150) lb2 (BACKUP, prio 100)
Nginx/HAProxy Nginx/HAProxy
└──────── backends: app1, app2, app3 ────────┘
- Both nodes run keepalived and exchange VRRP advertisements about once per second.
- The node with the highest priority holds the VIP (adds it to its interface).
- If the master stops advertising (crash, reboot, network failure) or its health check fails, the backup claims the VIP and sends gratuitous ARP, so the network learns the new location.
Important: the network must allow it
A floating IP needs both nodes on the same layer 2 network (or a provider feature for moving IPs), and the provider must allow the VIP to move between machines:
- Private networks / VLANs between your servers, and dedicated servers in the same rack, usually work.
- Public IPs on VPSes often can’t float freely, because the provider routes each IP to a specific server. Many providers offer failover IPs or floating IPs managed through their API or panel, and keepalived then calls that API in a notify script instead of relying on ARP.
- Multicast is often blocked on cloud networks. Use unicast VRRP (below).
Check with your provider before designing around a VIP. Alternatives without a VIP: DNS failover with health checks, or two load balancer IPs in DNS. See load balancer types.
Install
On both nodes:
sudo apt install keepalived # or: dnf install keepalived
Configuration
lb1 (/etc/keepalived/keepalived.conf):
global_defs {
router_id lb1
script_user root
enable_script_security
}
vrrp_script chk_haproxy {
script "/usr/bin/systemctl is-active --quiet haproxy"
interval 2
fall 2
rise 2
}
vrrp_instance VI_1 {
state MASTER
interface eth1
virtual_router_id 51
priority 150
advert_int 1
unicast_src_ip 10.0.0.11
unicast_peer {
10.0.0.12
}
authentication {
auth_type PASS
auth_pass s3cr3tpw
}
virtual_ipaddress {
10.0.0.100/24 dev eth1
}
track_script {
chk_haproxy
}
}
lb2 is identical, except for router_id lb2, state BACKUP, priority 100, and swapped unicast_src_ip / unicast_peer.
Notes:
virtual_router_idmust match on both nodes, and be unique on that network segment.track_script: if HAProxy (or Nginx:systemctl is-active --quiet nginx) dies, the node drops out of master, even though the server itself is up.auth_passis limited to 8 characters, and isn’t real security. Run VRRP on a private network.
Start on both nodes:
sudo systemctl enable --now keepalived
ip addr show eth1 | grep 10.0.0.100 # present only on the master
Allow VRRP between the nodes in the firewall. VRRP is IP protocol 112:
sudo iptables -A INPUT -p vrrp -s 10.0.0.12 -j ACCEPT
# UFW: add "-A ufw-before-input -p vrrp -s 10.0.0.12 -j ACCEPT" to /etc/ufw/before.rules
Let services bind to the VIP
The backup node doesn’t have the VIP, so HAProxy or Nginx configured to bind 10.0.0.100:80 fails to start there. Either bind to 0.0.0.0 (all addresses), or allow binding to non-local IPs:
echo 'net.ipv4.ip_nonlocal_bind=1' | sudo tee /etc/sysctl.d/99-nonlocal.conf && sudo sysctl --system
Provider floating IPs: notify scripts
When the IP must be moved through an API instead of ARP:
vrrp_instance VI_1 {
…
notify_master "/usr/local/bin/claim-floating-ip.sh"
}
The script calls the provider’s API to assign the floating IP to this server. Test it thoroughly. API latency adds to failover time.
Test failover
# on a client, keep requesting through the VIP
while true; do curl -s -o /dev/null -w '%{http_code} ' http://10.0.0.100/; sleep 0.5; done
# on lb1, simulate failure
sudo systemctl stop haproxy # or: sudo systemctl stop keepalived / reboot
You should see a brief gap (a second or two), and then responses continue from lb2. Check the logs with journalctl -u keepalived. When lb1 recovers, it takes the VIP back, because it has the higher priority. Add nopreempt (with both nodes set to state BACKUP) if you prefer to avoid a second switch.
Keep both nodes identical
Failover only helps if the backup can actually serve. Sync configs (Ansible, rsync, or a Git-based deploy), certificates and HAProxy or Nginx settings. Test the backup regularly. See Ansible playbooks.
Quick reference
| Item | Setting |
|---|---|
| Shared VIP | virtual_ipaddress { 10.0.0.100/24 dev eth1 } |
| Same on both nodes | virtual_router_id, auth_pass, VIP |
| Master / backup | state + priority |
| No multicast | unicast_src_ip + unicast_peer |
| Service health | vrrp_script + track_script |
| Bind before owning the VIP | net.ipv4.ip_nonlocal_bind=1 |
| Logs | journalctl -u keepalived |
Related: HAProxy load balancer and high availability on VPS.