Introduction
When a single server is no longer enough to handle load, you move to load balancing: a front proxy distributes requests to multiple backends. HAProxy is the open-source reference load balancer.
Use cases:
- Website → 3 Nginx backend servers, auto failover if one dies
- API → round-robin distribution with sticky sessions
- Database → MySQL/PostgreSQL read replica load balancing
- Centralized SSL termination + advanced healthchecks
Prerequisites
- Linux VPS dedicated to the load balancer (Debian 12 / Ubuntu 22.04+)
- At least 2 backend servers already in place
- A domain name (optional but recommended)
Step 1: Installation
sudo apt update
sudo apt install -y haproxy
sudo systemctl enable --now haproxy
sudo systemctl status haproxy
Recommended version: HAProxy 2.8+ (LTS).
Step 2: Target architecture
┌──────────────┐
Clients ────────▶│ HAProxy │
│ (LB VPS) │
└──────┬───────┘
│
┌────────────┼────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Web 1 │ │ Web 2 │ │ Web 3 │
│ 10.0.0.10│ │10.0.0.11│ │10.0.0.12│
└─────────┘ └─────────┘ └─────────┘
Backends communicate privately (ideally via private network between VPS, or via WireGuard).
Step 3: Minimal config (HTTP)
sudo nano /etc/haproxy/haproxy.cfg
global
log /dev/log local0
log /dev/log local1 notice
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin
stats timeout 30s
user haproxy
group haproxy
daemon
maxconn 50000
# Modern SSL
ssl-default-bind-ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
defaults
log global
mode http
option httplog
option dontlognull
option http-keep-alive
option forwardfor
timeout connect 5s
timeout client 60s
timeout server 60s
timeout http-request 10s
timeout http-keep-alive 15s
maxconn 50000
# Internal stats UI
listen stats
bind 127.0.0.1:8404
stats enable
stats uri /
stats refresh 10s
stats auth admin:AStrongPassword
# Frontend
frontend http_front
bind *:80
default_backend web_servers
# Backend
backend web_servers
balance roundrobin
option httpchk GET /health
http-check expect status 200
server web1 10.0.0.10:80 check inter 2000 rise 2 fall 3
server web2 10.0.0.11:80 check inter 2000 rise 2 fall 3
server web3 10.0.0.12:80 check inter 2000 rise 2 fall 3
Important details:
balance roundrobin: fair distribution. Others:leastconn(fewest active connections),source(sticky by IP)option httpchk GET /health: healthcheck on/health(your app must respond 200)check inter 2000 rise 2 fall 3: check every 2s, considered up after 2 successes, down after 3 failures
Validate config:
sudo haproxy -c -f /etc/haproxy/haproxy.cfg
Reload:
sudo systemctl reload haproxy
Step 4: Implement healthcheck endpoint on backend
On each backend server, add a /health endpoint returning 200 if app is healthy:
Nginx
server {
location = /health {
access_log off;
return 200 "OK";
add_header Content-Type text/plain;
}
}
Node.js
app.get('/health', (req, res) => {
res.status(200).send('OK');
});
More advanced: also check MySQL and Redis in the healthcheck to avoid routing to a backend with a dead DB.
Step 5: Stats UI
Open http://127.0.0.1:8404 via SSH tunnel:
ssh -L 8404:127.0.0.1:8404 user@HAPROXY_VPS_IP
Then http://localhost:8404 in your browser. Credentials: admin / AStrongPassword.
You see:
- Backend status (UP/DOWN, green/red)
- Per-server statistics
- Real-time load
Step 6: Add SSL (termination)
HAProxy can terminate SSL and talk HTTP to backends (common architecture).
Prepare the certificate
With Let's Encrypt:
sudo apt install -y certbot
sudo systemctl stop haproxy
sudo certbot certonly --standalone -d your-domain.com
sudo systemctl start haproxy
Combine cert + key into a single file for HAProxy:
sudo bash -c "cat /etc/letsencrypt/live/your-domain.com/fullchain.pem /etc/letsencrypt/live/your-domain.com/privkey.pem > /etc/ssl/haproxy.pem"
sudo chmod 600 /etc/ssl/haproxy.pem
Edit the config
frontend https_front
bind *:80
bind *:443 ssl crt /etc/ssl/haproxy.pem alpn h2,http/1.1
# Force HTTPS
http-request redirect scheme https unless { ssl_fc }
# HSTS
http-response set-header Strict-Transport-Security "max-age=31536000; includeSubDomains"
default_backend web_servers
Reload:
sudo systemctl reload haproxy
Step 7: Auto SSL renewal with hook
So Let's Encrypt renews without breaking HAProxy:
sudo nano /etc/letsencrypt/renewal-hooks/deploy/haproxy.sh
#!/bin/bash
DOMAIN=your-domain.com
cat /etc/letsencrypt/live/${DOMAIN}/fullchain.pem /etc/letsencrypt/live/${DOMAIN}/privkey.pem > /etc/ssl/haproxy.pem
chmod 600 /etc/ssl/haproxy.pem
systemctl reload haproxy
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/haproxy.sh
Step 8: Load balancing algorithms
Based on your case:
backend web_servers
# Round-robin (default)
balance roundrobin
# Least connections
balance leastconn
# Source IP hashing (natural sticky session)
balance source
# URI hashing (cache-friendly)
balance uri
# Cookie-based sticky session
cookie SERVERID insert indirect nocache
server web1 10.0.0.10:80 cookie web1 check
server web2 10.0.0.11:80 cookie web2 check
Step 9: ACLs (advanced routing)
Route by URL, header or IP:
frontend https_front
bind *:443 ssl crt /etc/ssl/haproxy.pem
# Path routing
acl is_api path_beg /api
acl is_admin path_beg /admin
# Hostname routing
acl host_blog hdr(host) -i blog.your-domain.com
# IP routing (limit /admin)
acl admin_whitelist src 82.26.157.10
# Apply
use_backend api_servers if is_api
use_backend admin_servers if is_admin admin_whitelist
use_backend blog_servers if host_blog
default_backend web_servers
Step 10: TCP load balancing (MySQL, Redis, SSH)
HAProxy can also load-balance TCP protocols (mode tcp):
frontend mysql_front
bind *:3306
mode tcp
default_backend mysql_servers
backend mysql_servers
mode tcp
balance leastconn
option mysql-check user haproxy_check
server db1 10.0.0.20:3306 check
server db2 10.0.0.21:3306 check backup # backup = used if db1 down
Step 11: Limit connections (anti-flood)
frontend https_front
bind *:443 ssl crt /etc/ssl/haproxy.pem
# Stick table (per-IP counter)
stick-table type ipv6 size 100k expire 30s store conn_rate(10s),http_req_rate(10s)
# Limits
http-request track-sc0 src
http-request reject if { sc_conn_rate(0) gt 50 } # > 50 connections/10s = block
http-request reject if { sc_http_req_rate(0) gt 200 } # > 200 req/10s = block
Step 12: Logs and monitoring
# Live logs
sudo tail -f /var/log/haproxy.log
# Errors only
sudo grep -i error /var/log/haproxy.log
# Top backends by requests
sudo awk '{print $7}' /var/log/haproxy.log | sort | uniq -c | sort -rn | head -10
For continuous monitoring: see the Prometheus + Grafana tutorial with haproxy_exporter.
Troubleshooting
"no backend available"
All your backends are marked DOWN. Check healthchecks:
curl http://10.0.0.10/health
Must return 200 OK.
"503 Service Unavailable"
Either no backend available, or server timeout too short. Increase timeout server.
SSL handshake errors
The .pem file is malformed. Regenerate:
cat fullchain.pem privkey.pem > haproxy.pem
Order matters: chain then key.
Stats UI not accessible
Check port 8404 isn't blocked by UFW. Prefer SSH tunneling over exposing it.
Useful commands
# Test config
sudo haproxy -c -f /etc/haproxy/haproxy.cfg
# Reload (zero downtime)
sudo systemctl reload haproxy
# View active sockets
sudo socat /run/haproxy/admin.sock - <<< "show info"
# Disable a server (maintenance)
sudo socat /run/haproxy/admin.sock - <<< "disable server web_servers/web1"
# Re-enable
sudo socat /run/haproxy/admin.sock - <<< "enable server web_servers/web1"
# View backend states
sudo socat /run/haproxy/admin.sock - <<< "show servers state"
# Reset stats
sudo socat /run/haproxy/admin.sock - <<< "clear counters"
Conclusion
With HAProxy in front:
- Your infra tolerates a backend failure (auto failover)
- You distribute load across N servers
- You centralize SSL termination
- You protect against application-level flood
Going further:
- Set up two HAProxy in HA with keepalived (virtual IP failover)
- Use HAProxy Data Plane API for automated driving
- Migrate to Traefik if you want native Docker/Kubernetes service discovery
Resources
- Official site: https://www.haproxy.org
- Documentation: https://docs.haproxy.org
- Configuration manual: https://docs.haproxy.org/2.8/configuration.html

















