HAProxy is the industry-standard open-source load balancer, trusted by GitHub, Stack Overflow, and Tumblr to handle millions of concurrent connections. While Nginx can reverse proxy HTTP traffic effectively, HAProxy was purpose-built for load balancing — offering superior health checking, connection draining, advanced ACL routing, and native TCP proxying that Nginx only partially supports. If you're running multiple backend servers, scaling horizontally across VPS instances, or need to load balance non-HTTP protocols like PostgreSQL or RabbitMQ, HAProxy is the right tool.
This guide covers HAProxy installation and configuration on Ubuntu VPS, from basic HTTP load balancing through advanced TCP proxying, TLS termination, real-time stats monitoring, and ACL-based routing. By the end, you'll have a production-grade load balancer capable of routing traffic intelligently across your infrastructure.
MassiveGRID Ubuntu VPS includes: Ubuntu 24.04 LTS pre-installed · Proxmox HA cluster with automatic failover · Ceph 3x replicated NVMe storage · Independent CPU/RAM/storage scaling · 12 Tbps DDoS protection · 4 global datacenter locations · 100% uptime SLA · 24/7 human support rated 9.5/10
Deploy a self-managed VPS — from $1.99/mo
Need dedicated resources? — from $19.80/mo
Want fully managed hosting? — we handle everything
When HAProxy vs Nginx Reverse Proxy
If you're already running Nginx as a web server and just need to forward requests to one or two upstream application processes on the same machine, Nginx reverse proxying is simpler and perfectly adequate. You can set up an Nginx reverse proxy on Ubuntu VPS in minutes with minimal configuration.
Choose HAProxy when your requirements go beyond basic reverse proxying:
- Multiple backend servers — HAProxy's health checking, load balancing algorithms, and connection draining are more mature than Nginx's upstream module
- TCP/Layer 4 load balancing — HAProxy natively proxies any TCP protocol (databases, message queues, custom protocols), while Nginx requires the stream module and has limited health check options
- Advanced routing — HAProxy ACLs can route based on HTTP headers, paths, cookies, SNI hostnames, source IP ranges, and connection rates — all evaluated at wire speed
- Connection draining — HAProxy can gracefully drain connections from a backend before removing it, enabling true zero-downtime deployments
- Real-time stats — HAProxy includes a built-in statistics dashboard showing per-backend and per-server metrics with no additional tools needed
For many production architectures, both tools coexist: HAProxy sits at the edge as the load balancer, distributing traffic to multiple VPS instances each running Nginx as the local web server.
HAProxy Fundamentals
HAProxy configuration revolves around four core concepts:
- Frontend — defines the listening socket (IP:port) and rules for accepting client connections. Frontends inspect incoming requests and decide which backend receives them.
- Backend — defines a group of servers that handle requests. Backends specify the load balancing algorithm, health checks, and server parameters.
- Listen — combines a frontend and backend in a single section. Useful for simple TCP proxying where you don't need separate routing rules.
- ACLs — access control lists that match request properties (path, headers, source IP) and drive routing decisions within frontends.
The configuration file is processed top-down with four major sections: global (process-level settings), defaults (default values inherited by all frontends/backends), then any number of frontend, backend, and listen sections.
Prerequisites
You need an Ubuntu 24.04 VPS with root or sudo access. HAProxy handles thousands of connections per second on minimal resources. A Cloud VPS with 1 vCPU / 1 GB RAM runs HAProxy as a dedicated load balancer comfortably for most workloads.
If you're load balancing across multiple backend servers, you'll need at least two additional VPS instances running your application. Ensure all servers can communicate over your private network or public IPs with appropriate firewall rules.
Installing HAProxy on Ubuntu 24.04
Ubuntu 24.04's default repositories include HAProxy, but for the latest stable release with the newest features, use the official HAProxy PPA:
# Add the official HAProxy PPA for version 3.0
sudo add-apt-repository ppa:vbernat/haproxy-3.0 -y
sudo apt update
# Install HAProxy
sudo apt install haproxy -y
# Verify installation
haproxy -v
# HAProxy version 3.0.x ...
# Enable HAProxy to start on boot
sudo systemctl enable haproxy
The main configuration file is /etc/haproxy/haproxy.cfg. Before making changes, back up the default configuration:
sudo cp /etc/haproxy/haproxy.cfg /etc/haproxy/haproxy.cfg.bak
Always validate your configuration before applying changes:
sudo haproxy -c -f /etc/haproxy/haproxy.cfg
HTTP Load Balancing
The most common use case: distributing HTTP traffic across multiple backend application servers. This configuration load balances across three web servers:
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 4096
defaults
log global
mode http
option httplog
option dontlognull
option forwardfor
timeout connect 5s
timeout client 30s
timeout server 30s
retries 3
errorfile 400 /etc/haproxy/errors/400.http
errorfile 403 /etc/haproxy/errors/403.http
errorfile 500 /etc/haproxy/errors/500.http
errorfile 502 /etc/haproxy/errors/502.http
errorfile 503 /etc/haproxy/errors/503.http
errorfile 504 /etc/haproxy/errors/504.http
frontend http_front
bind *:80
default_backend web_servers
backend web_servers
balance roundrobin
option httpchk GET /health
http-check expect status 200
server web1 10.0.1.10:8080 check inter 5s fall 3 rise 2
server web2 10.0.1.11:8080 check inter 5s fall 3 rise 2
server web3 10.0.1.12:8080 check inter 5s fall 3 rise 2
Key parameters in the server lines: check enables health checking, inter 5s runs checks every 5 seconds, fall 3 marks a server down after 3 consecutive failures, and rise 2 requires 2 consecutive successes before marking it healthy again.
Apply the configuration:
sudo haproxy -c -f /etc/haproxy/haproxy.cfg && sudo systemctl reload haproxy
TCP Load Balancing
HAProxy excels at TCP (Layer 4) load balancing for non-HTTP protocols. Unlike Nginx, HAProxy's TCP mode supports full health checking, connection limits, and graceful draining. Here are configurations for common TCP services:
PostgreSQL Load Balancing
listen postgresql
bind *:5432
mode tcp
option pgsql-check user haproxy_check
balance leastconn
timeout connect 10s
timeout client 60m
timeout server 60m
server pg1 10.0.1.20:5432 check inter 10s fall 3 rise 2
server pg2 10.0.1.21:5432 check inter 10s fall 3 rise 2 backup
The option pgsql-check directive performs an actual PostgreSQL authentication handshake, verifying the database is accepting connections — not just that the port is open. The backup flag on pg2 means it only receives traffic when pg1 is down.
RabbitMQ AMQP Load Balancing
listen rabbitmq_amqp
bind *:5672
mode tcp
balance roundrobin
timeout connect 10s
timeout client 3h
timeout server 3h
option clitcpka
server rabbit1 10.0.1.30:5672 check inter 10s fall 3 rise 2
server rabbit2 10.0.1.31:5672 check inter 10s fall 3 rise 2
server rabbit3 10.0.1.32:5672 check inter 10s fall 3 rise 2
Note the long timeouts — AMQP connections are long-lived, and option clitcpka enables TCP keepalive to detect dead connections.
Redis Sentinel-Aware Proxy
listen redis_primary
bind *:6379
mode tcp
balance first
option tcp-check
tcp-check connect
tcp-check send "PING\r\n"
tcp-check expect string +PONG
tcp-check send "INFO replication\r\n"
tcp-check expect string role:master
tcp-check send "QUIT\r\n"
tcp-check expect string +OK
server redis1 10.0.1.40:6379 check inter 5s fall 2 rise 1
server redis2 10.0.1.41:6379 check inter 5s fall 2 rise 1
server redis3 10.0.1.42:6379 check inter 5s fall 2 rise 1
This advanced TCP check sends Redis commands and verifies the server reports itself as master. Only the current master receives traffic, with automatic failover when Sentinel promotes a new master.
Health Checks
Effective health checking is what separates a load balancer from a simple reverse proxy. HAProxy supports multiple health check types:
- TCP check (default with
check) — verifies TCP connection succeeds - HTTP check — sends an HTTP request and validates the response
- Application-specific checks —
pgsql-check,mysql-check,redis-check,smtp-check - Custom TCP checks — send/expect sequences for any protocol
For HTTP backends, always implement a dedicated health endpoint in your application:
backend api_servers
balance roundrobin
option httpchk GET /api/health
http-check expect status 200
http-check expect header content-type -m sub application/json
# Aggressive checks for fast failover
server api1 10.0.1.10:3000 check inter 3s fall 2 rise 2
server api2 10.0.1.11:3000 check inter 3s fall 2 rise 2
# Conservative checks for stable backends
# server api3 10.0.1.12:3000 check inter 10s fall 5 rise 3
Tune intervals based on your tolerance for downtime detection versus health check overhead. A 3-second interval with fall 2 detects failures in 6 seconds. A 10-second interval with fall 5 takes 50 seconds but generates less health check traffic.
Load Balancing Algorithms
HAProxy supports several load balancing algorithms. Choose based on your workload characteristics:
- roundrobin — distributes requests sequentially across servers. Best for stateless applications with uniform request processing times. Supports dynamic weight adjustments.
- leastconn — sends new connections to the server with the fewest active connections. Best for long-lived connections (databases, WebSockets) or requests with variable processing times.
- source — hashes the client IP to consistently route the same client to the same backend. Provides session affinity without cookies, but creates imbalanced loads with few clients.
- uri — hashes the request URI for cache-friendly distribution. Each unique URL always hits the same backend, improving cache hit rates.
- hdr(name) — hashes a specific HTTP header value. Useful for routing based on API keys, tenant IDs, or custom headers.
# Session affinity via cookie insertion
backend web_servers
balance roundrobin
cookie SERVERID insert indirect nocache
server web1 10.0.1.10:8080 check cookie s1
server web2 10.0.1.11:8080 check cookie s2
HAProxy inserts a SERVERID cookie on the first response. Subsequent requests from the same client are routed to the same backend server. This is more reliable than source-based affinity because it works correctly behind NATs and CDNs.
TLS Termination with HAProxy
HAProxy can terminate TLS connections, decrypting traffic at the load balancer and forwarding plain HTTP to backends. This centralizes certificate management and offloads cryptographic work from application servers.
TLS termination at high traffic volumes is CPU-bound — each handshake requires asymmetric cryptography. A Dedicated VPS with guaranteed CPU ensures consistent handshake performance without noisy-neighbor variability.
# Combine certificate and private key into a single PEM file
sudo cat /etc/letsencrypt/live/example.com/fullchain.pem \
/etc/letsencrypt/live/example.com/privkey.pem \
> /etc/haproxy/certs/example.com.pem
sudo chmod 600 /etc/haproxy/certs/example.com.pem
Then configure the frontend for TLS:
frontend https_front
bind *:443 ssl crt /etc/haproxy/certs/example.com.pem alpn h2,http/1.1
bind *:80
# Redirect HTTP to HTTPS
http-request redirect scheme https unless { ssl_fc }
# Pass original protocol info to backends
http-request set-header X-Forwarded-Proto https if { ssl_fc }
http-request set-header X-Real-IP %[src]
# HSTS header
http-response set-header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
default_backend web_servers
For multiple domains, HAProxy supports SNI-based certificate selection. Place all PEM files in a directory and HAProxy selects the correct one based on the requested hostname:
bind *:443 ssl crt /etc/haproxy/certs/ alpn h2,http/1.1
HAProxy Stats Dashboard
HAProxy includes a built-in real-time statistics dashboard that shows traffic rates, backend health status, connection counts, and error rates per server — without installing any additional monitoring tools:
frontend stats
bind *:8404
mode http
stats enable
stats uri /stats
stats refresh 10s
stats admin if LOCALHOST
stats auth admin:your_secure_password
stats hide-version
Access the dashboard at http://your-vps-ip:8404/stats. The dashboard shows:
- Current sessions and session rate per frontend/backend
- Bytes in/out per server
- Backend server status (UP, DOWN, DRAIN, MAINT)
- HTTP response code distribution (2xx, 3xx, 4xx, 5xx)
- Average queue time and response time per server
For programmatic access, the stats socket provides a Unix socket interface for scripting and integration with external monitoring:
# Query server states
echo "show stat" | sudo socat stdio /run/haproxy/admin.sock | cut -d, -f1,2,18
# Drain a server before maintenance
echo "set server web_servers/web1 state drain" | sudo socat stdio /run/haproxy/admin.sock
# Set server to maintenance mode
echo "set server web_servers/web1 state maint" | sudo socat stdio /run/haproxy/admin.sock
Connection Draining for Zero-Downtime Deploys
Connection draining lets you remove a backend server from the rotation gracefully — existing connections complete normally while no new connections are sent to that server. This is essential for zero-downtime deployments.
The deployment workflow using HAProxy drain mode:
#!/bin/bash
# deploy.sh — Zero-downtime deploy via HAProxy drain
HAPROXY_SOCKET="/run/haproxy/admin.sock"
BACKEND="web_servers"
SERVERS=("web1" "web2" "web3")
for SERVER in "${SERVERS[@]}"; do
echo "--- Deploying to $SERVER ---"
# 1. Set server to drain mode (stop new connections)
echo "set server $BACKEND/$SERVER state drain" | \
socat stdio $HAPROXY_SOCKET
# 2. Wait for active connections to finish
while true; do
SESSIONS=$(echo "show stat" | socat stdio $HAPROXY_SOCKET | \
grep "^$BACKEND,$SERVER," | cut -d, -f5)
[ "$SESSIONS" -eq 0 ] && break
echo " Waiting for $SESSIONS active sessions..."
sleep 2
done
# 3. Deploy new code to this server
ssh deploy@${SERVER}-ip "cd /app && git pull && systemctl restart app"
# 4. Wait for application to be ready
sleep 5
# 5. Re-enable server
echo "set server $BACKEND/$SERVER state ready" | \
socat stdio $HAPROXY_SOCKET
echo " $SERVER deployed and re-enabled"
sleep 3 # Brief stabilization period
done
echo "Deployment complete"
This rolls through each server one at a time: drain, wait for connections to finish, deploy, re-enable. No client sees a dropped connection or error response.
ACLs for Advanced Routing
HAProxy ACLs (Access Control Lists) match request properties and drive routing decisions. They're what make HAProxy a true application delivery controller rather than just a load balancer.
Path-Based Routing
frontend http_front
bind *:80
# Route API requests to API backend
acl is_api path_beg /api/
acl is_websocket hdr(Upgrade) -i websocket
acl is_static path_end .css .js .png .jpg .svg .woff2
use_backend api_servers if is_api
use_backend ws_servers if is_websocket
use_backend static_servers if is_static
default_backend web_servers
Header-Based Routing
# Route by custom header (multi-tenant SaaS)
acl is_tenant_a hdr(X-Tenant-ID) -i tenant-a
acl is_tenant_b hdr(X-Tenant-ID) -i tenant-b
use_backend tenant_a_servers if is_tenant_a
use_backend tenant_b_servers if is_tenant_b
SNI-Based Routing (TCP Mode)
frontend tls_front
bind *:443
mode tcp
tcp-request inspect-delay 5s
tcp-request content accept if { req_ssl_hello_type 1 }
# Route different domains to different backends without terminating TLS
acl is_app1 req_ssl_sni -i app1.example.com
acl is_app2 req_ssl_sni -i app2.example.com
use_backend app1_tls if is_app1
use_backend app2_tls if is_app2
default_backend default_tls
SNI-based routing passes encrypted traffic through to the backend without terminating TLS at HAProxy. This is useful when backends need to handle their own certificates or when you want to avoid the CPU cost of double encryption.
Rate Limiting with ACLs
frontend http_front
bind *:80
# Track request rate per source IP
stick-table type ip size 100k expire 30s store http_req_rate(10s)
http-request track-sc0 src
# Deny if more than 100 requests per 10 seconds
acl rate_abuse sc_http_req_rate(0) gt 100
http-request deny deny_status 429 if rate_abuse
HAProxy in Multi-VPS Architecture
In a multi-VPS architecture, HAProxy typically runs on a dedicated VPS at the front of the stack, distributing traffic to backend application and database servers.
A typical production layout:
# VPS 1: HAProxy (load balancer)
# → Receives all incoming traffic
# → TLS termination
# → Routes to application VPS instances
#
# VPS 2-3: Application servers (Nginx + app)
# → Run your web application
# → Serve dynamic content
#
# VPS 4: Database server (PostgreSQL/MySQL)
# → Receives connections from app servers only
# → Not exposed to the internet
For high availability of the load balancer itself, run two HAProxy instances with keepalived and a floating IP. If the primary fails, the floating IP moves to the standby within seconds:
# /etc/keepalived/keepalived.conf on the primary HAProxy VPS
vrrp_script check_haproxy {
script "/usr/bin/killall -0 haproxy"
interval 2
weight 2
}
vrrp_instance VI_1 {
state MASTER
interface eth0
virtual_router_id 51
priority 101
advert_int 1
virtual_ipaddress {
203.0.113.100/24
}
track_script {
check_haproxy
}
}
Prefer Managed Load Balancing?
Load balancer configuration, health check tuning, TLS certificate management, and ACL rule maintenance are ongoing operational tasks that require attention every time your backend infrastructure changes. When you add a server, deploy a new service, or renew certificates, the load balancer configuration must be updated and tested.
With a fully managed server from MassiveGRID, the operations team handles load balancer configuration, health check tuning, TLS management, and traffic routing — so you can focus on the applications behind the load balancer rather than the infrastructure in front of them.