Install and Configure HAProxy
Install HAProxy and configure frontends, backends, ACL-based routing, TLS termination, sticky sessions, and HTTP health checks on modern Linux distros.
Before you start
- ▸Root or sudo access on the target server
- ▸One or more backend application servers reachable by IP
- ▸A valid TLS certificate and private key if configuring HTTPS
- ▸Ports 80 and 443 open in your firewall (ufw, firewalld, or nftables)
HAProxy is the de facto standard for high-availability load balancing on Linux. It handles millions of requests per second in production at companies like GitHub and Reddit, and it does so with a configuration file that is genuinely readable once you understand its structure. This guide walks through a full HAProxy setup: installing the daemon, defining frontends and backends, writing ACL rules, terminating TLS, enabling sticky sessions, and wiring up health checks. Commands are shown for Debian/Ubuntu, Fedora/RHEL, and Arch where they differ.
Install HAProxy
Debian / Ubuntu
The default apt repositories often carry an older HAProxy. Add the official PPA for the latest 2.x stable branch.
sudo add-apt-repository ppa:vbernat/haproxy-2.9
sudo apt update
sudo apt install haproxy
Fedora / RHEL 9 / Rocky Linux
sudo dnf install haproxy
Arch Linux
sudo pacman -S haproxy
Enable and start the service with systemd:
sudo systemctl enable --now haproxy
Verify the version you landed on:
haproxy -v
Output will resemble: HAProxy version 2.9.x .... Anything below 2.4 is end-of-life; upgrade before putting it in front of production traffic.
Configuration File Structure
HAProxy's configuration lives at /etc/haproxy/haproxy.cfg. It is divided into four sections: global (process-level settings), defaults (inherited by all frontends and backends), frontend (where traffic enters), and backend (where it goes). Always validate the file before reloading:
sudo haproxy -c -f /etc/haproxy/haproxy.cfg
Frontend and Backend Basics
Below is a minimal but realistic configuration for HTTP load balancing across three application servers. Replace the IP addresses with your actual backend hosts.
global
log /dev/log local0
log /dev/log local1 notice
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
stats timeout 30s
user haproxy
group haproxy
daemon
maxconn 50000
defaults
log global
mode http
option httplog
option dontlognull
option forwardfor
option http-server-close
timeout connect 5s
timeout client 30s
timeout server 30s
errorfile 400 /etc/haproxy/errors/400.http
errorfile 503 /etc/haproxy/errors/503.http
frontend http_in
bind *:80
default_backend app_servers
backend app_servers
balance roundrobin
server app1 192.168.1.10:8080 check
server app2 192.168.1.11:8080 check
server app3 192.168.1.12:8080 check
The balance directive controls the algorithm. Common choices are roundrobin, leastconn (best for long-lived connections like WebSockets), and source (IP hash). The check keyword on each server line enables TCP-level health checks by default.
ACLs: Content-Based Routing
Access Control Lists let you route traffic based on headers, paths, source IPs, and more. A common pattern is splitting API traffic from the main web application.
frontend http_in
bind *:80
# Define ACLs
acl is_api path_beg /api/
acl is_static path_end .css .js .png .jpg .svg .woff2
# Route based on ACLs
use_backend api_servers if is_api
use_backend static_servers if is_static
default_backend app_servers
backend api_servers
balance leastconn
server api1 192.168.1.20:3000 check
server api2 192.168.1.21:3000 check
backend static_servers
balance roundrobin
server cdn1 192.168.1.30:80 check
backend app_servers
balance roundrobin
server app1 192.168.1.10:8080 check
server app2 192.168.1.11:8080 check
ACL conditions can be combined with if / unless and Boolean operators. For example, use_backend admin_backend if is_admin_path !is_external_ip routes to an admin backend only when the path matches and the source is internal.
TLS Termination
HAProxy terminates TLS in the frontend, decrypting traffic before passing plain HTTP to your backends. You need a combined PEM file containing the certificate, any intermediates, and the private key in that order.
sudo cat /etc/ssl/certs/example.com.crt \
/etc/ssl/certs/ca-bundle.crt \
/etc/ssl/private/example.com.key \
| sudo tee /etc/haproxy/certs/example.com.pem
sudo chmod 640 /etc/haproxy/certs/example.com.pem
sudo chown root:haproxy /etc/haproxy/certs/example.com.pem
Update the frontend to bind on port 443 and reference the PEM file. Also add an HTTP-to-HTTPS redirect on port 80:
frontend http_redirect
bind *:80
http-request redirect scheme https code 301
frontend https_in
bind *:443 ssl crt /etc/haproxy/certs/example.com.pem
ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11
http-response set-header Strict-Transport-Security "max-age=63072000"
acl is_api path_beg /api/
use_backend api_servers if is_api
default_backend app_servers
The cipher suite list above enforces TLS 1.3 only. If you need TLS 1.2 compatibility, add no-tls13 removal and extend ssl-default-bind-ciphers with a TLS 1.2 suite list. Use haproxy -vv | grep OpenSSL to confirm which OpenSSL version and features are compiled in.
Sticky Sessions
Some applications store session state locally on the backend server. Sticky sessions ensure repeat requests from the same client always reach the same server. HAProxy implements this via cookies.
backend app_servers
balance roundrobin
cookie SERVERID insert indirect nocache
server app1 192.168.1.10:8080 check cookie app1
server app2 192.168.1.11:8080 check cookie app2
server app3 192.168.1.12:8080 check cookie app3
insert tells HAProxy to inject its own SERVERID cookie if the client does not already carry one. indirect strips the cookie before forwarding the request so your application never sees it. nocache prevents proxies from caching responses with this cookie. If a backend marked with a particular cookie value goes down, HAProxy fails the request to another server—it does not serve a 503 just because the sticky target is unhealthy.
Health Checks
The bare check keyword performs a TCP connect. For HTTP services you want a real application-level health check:
backend app_servers
balance roundrobin
option httpchk GET /healthz HTTP/1.1\r\nHost:\ app.internal
http-check expect status 200
default-server inter 5s fall 3 rise 2
server app1 192.168.1.10:8080 check
server app2 192.168.1.11:8080 check
server app3 192.168.1.12:8080 check
- inter 5s — check every 5 seconds.
- fall 3 — mark a server down after 3 consecutive failures.
- rise 2 — return it to rotation after 2 consecutive successes.
- http-check expect status 200 — any non-200 response marks the check failed.
You can also use http-check expect rstring OK to match a regex in the response body if your health endpoint returns a JSON payload.
Stats Dashboard
HAProxy ships a built-in statistics page. Add a dedicated frontend for it:
frontend stats
bind *:8404
stats enable
stats uri /stats
stats refresh 10s
stats auth admin:changeme
stats hide-version
After reloading, visit http://your-server:8404/stats. Protect this with firewall rules or bind to a management interface only—never expose it to the public internet with a weak password.
Apply Configuration and Verify
# Validate first
sudo haproxy -c -f /etc/haproxy/haproxy.cfg
# Reload without dropping connections (graceful)
sudo systemctl reload haproxy
# Check status
sudo systemctl status haproxy
Use reload, not restart. HAProxy supports hitless reloads via socket-passing; restarting drops in-flight connections.
Test TLS from the command line:
openssl s_client -connect your-server:443 -servername example.com < /dev/null 2>&1 | grep -E 'Protocol|Cipher'
Troubleshooting
- Permission denied on the stats socket: Ensure your user is in the
haproxygroup, or usesudo socat stdio /run/haproxy/admin.sock. - Backend shows DOWN immediately: Run
sudo journalctl -u haproxy -n 50and look for connection refused errors. Confirm the backend port is actually listening withss -tlnpon the backend host. - TLS handshake failure: Check that your PEM file contains the full chain.
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt /etc/haproxy/certs/example.com.pemshould return OK. - Sticky sessions not holding: Confirm the client is accepting and sending cookies. Use
curl -c cookiejar.txt -b cookiejar.txt https://example.com/to simulate. Also verifyindirectis set so the cookie survives the round trip. - Config validation passes but reload fails: Check available file descriptors. HAProxy can exhaust them under high
maxconn. Raise the system limit in/etc/security/limits.confor via a systemd override (LimitNOFILE=1000000).
Frequently asked questions
- What is the difference between HAProxy's frontend and backend?
- A frontend defines where HAProxy listens for incoming connections and which rules apply to arriving traffic. A backend defines the pool of servers that actually handle requests. One frontend can route to multiple backends using ACLs.
- Can HAProxy terminate TLS and also pass raw TCP through to the backend?
- Yes. Set mode tcp in the frontend and use 'pass-through' (no ssl crt directive) to forward the encrypted stream untouched. This is useful when your application servers handle TLS themselves, but you lose the ability to inspect HTTP headers.
- How do I reload HAProxy without dropping active connections?
- Use 'systemctl reload haproxy' instead of restart. HAProxy passes the listening sockets to the new process and drains old connections gracefully, achieving a hitless reload.
- Are sticky sessions reliable when a backend server fails?
- No. When the target backend is marked down, HAProxy will route the client to another server, breaking session affinity. Sticky sessions are not a substitute for proper server-side session sharing via Redis, a database, or a distributed cache.
- Does HAProxy support HTTP/2?
- Yes, from version 2.0 onwards. Add 'alpn h2,http/1.1' to the bind line on your HTTPS frontend to negotiate HTTP/2 via ALPN. Backend connections are typically HTTP/1.1 unless you explicitly configure h2 on the server lines.
Related guides
Build an Intranet Server on Linux
Set up a complete small-office intranet on one Linux box: Nginx web server, dnsmasq local DNS, Samba file sharing, and a Wiki.js team wiki.
Build an nftables Firewall Script
Build a complete nftables firewall from scratch: tables, chains, sets, default-deny input policy, service allowlisting, and persistent systemd configuration.
How to Change the Webmin Port
Move Webmin off its default port 10000 by editing miniserv.conf, updating your firewall (ufw, firewalld, or nftables), and restarting the service.
Configure a UPS on Linux with NUT
Install and configure Network UPS Tools (NUT) on Linux to detect your UPS, load the right driver, and trigger a safe automatic shutdown when battery runs low.