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:

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:

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:

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:

# 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:

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.