HAproxy
HAProxy + Keepalived Load Balancer Pair — Setup Guide
A step-by-step guide to build a highly available, active/passive load balancer pair on two Ubuntu servers using HAProxy (Layer 4 TCP passthrough) and Keepalived (VRRP virtual IP).
1. Overview
Two Linux servers run HAProxy. Keepalived manages a single floating Virtual IP (VIP) using VRRP. Only one node owns the VIP at any time (the MASTER). If it fails, the BACKUP node takes over the VIP within a few seconds. Clients only ever talk to the VIP.
Because TLS is passed through (not terminated), HAProxy runs in TCP mode: it forwards the raw encrypted TCP stream to the backend web servers, which hold the certificates. HAProxy never decrypts the traffic.
Environment
| Role | Hostname | IP Address | Notes |
|---|---|---|---|
| Load Balancer 1 | haproxy01 | 10.10.10.4 | Keepalived MASTER |
| Load Balancer 2 | haproxy02 | 10.10.10.5 | Keepalived BACKUP |
| Virtual IP (VIP) | — | 10.10.10.10 | Client-facing, floats between nodes |
| Web Server 1 | web01 | 10.10.11.1 | Backend |
| Web Server 2 | web02 | 10.10.11.2 | Backend |
Load-balanced ports: 443, 7443, 8443, 9443
Traffic flow
Client
│ connects to VIP 10.10.10.10 : {443,7443,8443,9443}
▼
Keepalived VIP (owned by MASTER node)
▼
HAProxy01 ──or── HAProxy02 (TCP passthrough, encrypted)
│
├──► WEB01 10.10.11.1
└──► WEB02 10.10.11.2Prerequisites
- Two Ubuntu servers (this guide targets a modern apt-based Ubuntu release).
- Root or
sudoaccess on both. - Static IPs configured:
10.10.10.4and10.10.10.5in the DMZ segment10.60.85.0/28. - Backend web servers reachable from both load balancers on the target ports.
- Firewall rules in place (see Section 9).
Do every step on BOTH servers unless a step is explicitly marked MASTER only or BACKUP only.
2. Prepare both servers
Run on both haproxy01 and haproxy02.
2.1 Update the OS
| |
2.2 Set hostnames (optional but recommended)
On haproxy01:
| |
On haproxy02:
| |
Add both hosts and the backends to /etc/hosts on each server:
| |
2.3 Set the timezone and enable time sync (recommended)
| |
3. Kernel networking settings
HAProxy on the BACKUP node must be able to bind to the VIP even though the VIP is not yet present on that node. Enable non-local binding and confirm IP forwarding is off (LB does not route).
Run on both servers:
| |
Verify:
| |
4. Install HAProxy and Keepalived
Run on both servers:
| |
Check versions:
| |
Do not enable them to auto-start yet — configure first, then start.
5. Configure HAProxy (TCP passthrough)
The HAProxy config is identical on both servers. Because we bind explicitly to the VIP, only the node currently holding the VIP will actually receive client traffic.
5.1 Back up the default config
| |
5.2 Write the new config
Run on both servers:
| |
5.3 Configuration notes
mode tcp— Layer 4. The TLS handshake and certificates stay end-to-end between the client and the web servers. HAProxy never sees plaintext.bind 10.10.10.10:<port>— HAProxy listens on the VIP only. Combined withip_nonlocal_bind = 1, both nodes can start even when they don’t currently hold the VIP.check— a plain TCP connect health check (opens the port, closes it). A backend is marked down afterfall 3consecutive failures and back up afterrise 2successes, probed everyinter 3s.balance roundrobin— alternates connections between web01 and web02. Change toleastconnif connections are long-lived, or addsticktables if you need session affinity (not usually needed for TLS passthrough).
5.4 Validate the config
Run on both servers:
| |
Do not start HAProxy yet — configure Keepalived first.
6. Configure Keepalived (VRRP virtual IP)
Keepalived config differs between the two nodes (state, priority). We also add a health check so the VIP fails over if HAProxy dies on the MASTER.
6.1 Health-check script (both servers)
| |
killall -0sends no signal; it only checks that at least onehaproxyprocess exists. Ensurepsmiscis installed (it usually is):sudo apt -y install psmisc.
6.2 Keepalived config — MASTER (haproxy01 only)
Run on haproxy01:
| |
6.3 Keepalived config — BACKUP (haproxy02 only)
Run on haproxy02:
| |
6.4 Important settings to check
interface eth0— replace with the real NIC name on each node. Find it with:It might be1ip -br addrens160,eth0,enp3s0, etc. Both nodes must point at the interface that carries the10.10.10.0/24subnet.virtual_router_id 51— must be the same on both nodes, and unique on the L2 segment (if another VRRP/HSRP group uses 51, pick a different number 1–255 on both).auth_pass— must match on both nodes. ChangeHA!VRRP24to your own value.unicast_src_ip/unicast_peer— uses VRRP unicast, which avoids multicast issues on NSX overlay segments. If your network requires multicast VRRP instead, remove both lines./24on the VIP matches the DMZ segment mask10.10.10.0/24.
6.5 Validate the Keepalived config
| |
7. Start the services
Start HAProxy first, then Keepalived, on both servers (MASTER first if you want a predictable initial owner).
| |
Check status:
| |
If you change a config later, reload without dropping connections:
| |
8. Verify the setup
8.1 Confirm the VIP is on the MASTER
On haproxy01 (MASTER):
| |
On haproxy02 (BACKUP):
| |
8.2 Watch Keepalived state transitions
| |
8.3 Confirm HAProxy is listening on the VIP
On the MASTER:
| |
8.4 Test the backends through the VIP
From a client (or from a load balancer itself):
| |
A returned certificate subject/expiry proves the passthrough path reaches a web server.
8.5 View the HAProxy stats dashboard
Open in a browser (from an allowed admin host):
http://10.10.10.4:8404/stats
http://10.10.10.5:8404/statsBackends should show UP (green). If you enabled stats auth, log in with those credentials.
9. Test failover
Simulate a MASTER failure and confirm the BACKUP takes over.
9.1 Failover by stopping HAProxy (tests the track script)
On haproxy01 (MASTER):
| |
Within a few seconds the health check drops haproxy01’s priority below haproxy02’s, so the VIP moves. Confirm on haproxy02:
| |
Restore:
| |
Because haproxy01 has the higher base priority, it reclaims the VIP (preemption is on by
default). To avoid the VIP flapping back, add nopreempt to the BACKUP’s vrrp_instance and
set haproxy01’s state to BACKUP as well (both nodes BACKUP + priority decides owner).
9.2 Failover by rebooting
| |
Confirm the VIP moves to the other node and clients keep connecting to 10.10.10.10.
10. Firewall requirements
Ensure these are permitted. VRRP is IP protocol 112.
| # | Source | Destination | Port / Proto | Purpose |
|---|---|---|---|---|
| 1 | Clients / DMZ proxy / VPN / DNATs | 10.10.10.10 (VIP) | TCP 443, 7443, 8443, 9443 | Client → VIP |
| 2 | 10.10.10.4, 10.10.10.5 | 10.10.11.1, 10.10.11.2 | TCP 443, 7443, 8443, 9443 | HAProxy → web servers |
| 3 | 10.10.10.4, 10.10.10.5 | 10.10.10.4, 10.10.10.5 | IP proto 112 (VRRP) | Keepalived peer sync |
| 4 | Admin hosts | 10.10.10.4, 10.10.10.5 | TCP 8404 | HAProxy stats page (optional) |
| 5 | Admin hosts | 10.10.10.4, 10.10.10.5 | TCP 22 | SSH management |
With unicast VRRP (as configured above) the peers exchange VRRP packets directly between
10.10.10.4and10.10.10.5, so rule #3 is unicast rather than multicast224.0.0.18.
11. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Both nodes hold the VIP (split-brain) | VRRP packets blocked between peers | Allow IP proto 112 (rule #3); verify unicast_peer IPs; check virtual_router_id matches |
| VIP never appears | Wrong interface name | Set the real NIC from ip -br addr |
| HAProxy won’t start on BACKUP: cannot bind socket | Non-local bind not set | Confirm net.ipv4.ip_nonlocal_bind = 1 (Section 3) |
| Backends show DOWN in stats | Web servers not listening / firewall | Test nc -vz 10.10.11.1 443 from the LB; check rule #2 |
| VIP flaps back and forth | Preemption on | Add nopreempt and set both nodes to state BACKUP |
| Keepalived ignores the check script | Script security | Ensure enable_script_security + script is chmod +x and root-owned |
| Clients get cert errors | Passthrough reaching wrong backend / SNI mismatch | Verify with openssl s_client (Section 8.4); confirm certs live on web01/web02 |
Useful commands
| |
(Install socat if needed: sudo apt -y install socat.)
12. Changing the VIP
To move the load balancer to a different Virtual IP, the VIP must be updated in two config files on both nodes (plus the cert and external dependencies if applicable). Changing it in only one place will break the service.
12.1 Keepalived — the floating VIP itself
File: /etc/keepalived/keepalived.conf on both haproxy01 and haproxy02.
virtual_ipaddress {
10.10.10.10/28 # <-- change to the new VIP (keep the correct prefix)
}If the new VIP is in a different subnet, also review:
- The
/28prefix — set it to the new subnet’s mask. interface— must be the NIC carrying the new subnet.unicast_src_ip/unicast_peer— only if the node IPs themselves change.
12.2 HAProxy — the bind lines
File: /etc/haproxy/haproxy.cfg on both nodes. Every frontend binds to the VIP — there are
four, one per port:
bind 10.10.10.10:443 # ft_443
bind 10.10.10.10:7443 # ft_7443
bind 10.10.10.10:8443 # ft_8443
bind 10.10.10.10:9443 # ft_9443Update all four at once (adjust the target IP):
| |
12.3 Certificate (only if TLS terminated / SAN lists the VIP)
This passthrough build has no cert on HAProxy, so no change is needed here. If you later switch
to TLS termination and the certificate’s subjectAltName includes the VIP by IP, reissue it with
the new VIP so IP-based clients don’t get a name-mismatch warning.
12.4 Apply — order matters
On both nodes, validate then reload Keepalived first (brings up the new VIP) and HAProxy second:
| |
Verify:
| |
12.5 Don’t forget the external dependencies
- Firewall / NSX DFW
- DNS
13. Quick reference
| Item | Value |
|---|---|
| VIP | 10.10.10.10 |
| MASTER | haproxy01 — 10.10.10.4 (priority 150) |
| BACKUP | haproxy02 — 10.10.10.5 (priority 100) |
| Backends | web01 10.10.11.1, web02 10.10.11.2 |
| Ports | 443, 7443, 8443, 9443 |
| Mode | TCP passthrough (Layer 4, no TLS termination) |
| Health check | TCP connect (check) |
| VRRP id | 51 (unicast) |
| Stats page | http://<node>:8404/stats |
Change log
- v1.0 — Initial guide: HAProxy TCP passthrough + Keepalived unicast VRRP, TCP health checks.