
This guide lets you deploy HAProxy to spread HTTP traffic across several backend servers. You will configure round-robin load balancing, automatic health checks, sticky sessions and protection against abuse. At the end, you will have a reverse proxy ready for production.
Audience: system administrators, DevOps engineers, developers who want to improve the availability of their applications.
Prerequisites: a Linux server (Debian 13, Ubuntu 24.04 or RHEL 10) with root access, and at least two backend servers (or Docker containers).
What you will learn
- Build HAProxy 3.2 from a verified archive and run it under systemd.
- Spread traffic across several backends and remove a failing server automatically.
- Route requests by path or header with ACLs.
- Harden the entry point: TLS, security headers, rate limiting, blocklist.
- Drive the service live through the administration socket, without a reload.
What is HAProxy?
HAProxy (High Availability Proxy) is an open source load balancer and reverse proxy. Created in 2000 by Willy Tarreau, it is used at large scale in production to handle millions of simultaneous connections.
Why use HAProxy?
| Without HAProxy | With HAProxy |
|---|---|
| A single server receives all the traffic | Traffic is spread across several servers |
| If the server goes down, the site is unavailable | Traffic fails over automatically to the healthy servers |
| No protection against load spikes | Queue management and rate limiting |
| TLS configured on every server | Centralised TLS termination |
Prerequisites
Installing HAProxy 3.2
This guide uses HAProxy 3.2.25, the latest release of the 3.2 branch as of 30 September 2026. The choice makes sense twice over on the security side: the 3.2 branch is LTS (security fixes until ~2030) and it is the one that went through the security audit commissioned by ANSSI (on 3.2.5). You install the latest 3.2.x to combine the branch audit with all its fixes.
For a secure installation, build from the official sources downloaded from haproxy.org:
-
Install the build dependencies:
Terminal window sudo apt install -y build-essential libssl-dev libpcre2-dev zlib1g-dev liblua5.4-dev -
Download and extract the sources:
Terminal window cd /tmpcurl -LO https://www.haproxy.org/download/3.2/src/haproxy-3.2.25.tar.gzcurl -LO https://www.haproxy.org/download/3.2/src/haproxy-3.2.25.tar.gz.sha256sha256sum -c haproxy-3.2.25.tar.gz.sha256tar xzf haproxy-3.2.25.tar.gzcd haproxy-3.2.25The
.sha256file published next to the archive lets you verify it has not been tampered with:sha256sum -cmust answerhaproxy-3.2.25.tar.gz: OKbefore any build. -
Build with the recommended options:
Terminal window make -j$(nproc) TARGET=linux-glibc USE_OPENSSL=1 USE_PCRE2=1 USE_ZLIB=1 USE_LUA=1 USE_PROMEX=1Build options:
USE_OPENSSL=1: TLS supportUSE_PCRE2=1: fast regular expressionsUSE_ZLIB=1: HTTP compressionUSE_LUA=1: Lua scriptingUSE_PROMEX=1: built-in Prometheus exporter
-
Install:
Terminal window sudo make install PREFIX=/usr/local -
Check the installation:
Terminal window /usr/local/sbin/haproxy -vYou should see:
HAProxy version 3.2.25-70469d3 2026/09/28 - https://haproxy.org/Status: long-term supported branch - will stop receiving fixes around Q2 2030.
For a secure installation, build from the official sources:
-
Install the build dependencies:
Terminal window sudo dnf install -y gcc make openssl-devel pcre2-devel zlib-devel lua-devel systemd-devel -
Download and extract the sources:
Terminal window cd /tmpcurl -LO https://www.haproxy.org/download/3.2/src/haproxy-3.2.25.tar.gzcurl -LO https://www.haproxy.org/download/3.2/src/haproxy-3.2.25.tar.gz.sha256sha256sum -c haproxy-3.2.25.tar.gz.sha256tar xzf haproxy-3.2.25.tar.gzcd haproxy-3.2.25The
.sha256file published next to the archive lets you verify it has not been tampered with:sha256sum -cmust answerhaproxy-3.2.25.tar.gz: OKbefore any build. -
Build with the recommended options:
Terminal window make -j$(nproc) TARGET=linux-glibc USE_OPENSSL=1 USE_PCRE2=1 USE_ZLIB=1 USE_LUA=1 USE_PROMEX=1 -
Install:
Terminal window sudo make install PREFIX=/usr/local -
Check the installation:
Terminal window /usr/local/sbin/haproxy -v
HAProxy key concepts
Before configuring HAProxy, let us understand the building blocks of its architecture.
Frontend, Backend and Listen
HAProxy uses three types of configuration blocks:
| Block | Role |
|---|---|
| frontend | Entry point that receives client connections |
| backend | Group of servers that handle the requests |
| listen | Combines frontend + backend in a single block |
The diagram follows the path of a request: the frontend receives it, then the backend picks a server according to the declared load-balancing algorithm.
Load-balancing algorithms
HAProxy offers several methods to distribute traffic:
| Algorithm | Description | Use case |
|---|---|---|
roundrobin | Hands out requests in turn | Identical servers, balanced load |
leastconn | Sends to the server with the fewest connections | Long requests (downloads, WebSocket) |
source | Hash of the source IP (sticky) | Session affinity without a cookie |
uri | Hash of the URI | Distributed cache |
random | Random selection | Simple and effective at large scale |
ACLs (Access Control Lists)
ACLs let you take routing decisions based on conditions:
acl is_api path_beg /api # True if the path starts with /apiacl is_admin hdr(host) -i admin.example.com # True if Host = admin...
use_backend api_servers if is_apiuse_backend admin_servers if is_admindefault_backend web_serversCommon condition types:
| Condition | Description |
|---|---|
path_beg /api | The path starts with "/api" |
path_end .jpg | The path ends with ".jpg" |
hdr(host) -i example.com | The Host header is "example.com" |
src 192.168.1.0/24 | The source IP is in that subnet |
Health Checks
Health checks automatically verify that your backend servers work:
- TCP check: checks that the port answers (default)
- HTTP check: sends an HTTP request and expects a 200 code
- If a server does not answer, HAProxy removes it automatically from the pool
Basic configuration
Now that you understand the concepts, let us move to practice. The main configuration file is /etc/haproxy/haproxy.cfg. The configuration below is a working starting point you can copy, then adapt to your infrastructure.
What this configuration does:
- Listens on port 80 (HTTP)
- Spreads traffic across two web servers (192.168.1.10 and 192.168.1.11)
- Automatically checks that the servers answer (health checks)
- Provides a secured statistics page on port 8404
global log /dev/log local0 log /dev/log local1 notice maxconn 4096 user haproxy group haproxy daemon
defaults mode http log global option httplog option dontlognull timeout connect 5s timeout client 30s timeout server 30s timeout http-request 10s
# Statistics page (secured)listen stats bind 127.0.0.1:8404 stats enable stats uri /stats stats refresh 10s stats show-legends stats auth admin:CHANGE_THIS_PASSWORD
# HTTP frontendfrontend http_front bind *:80 default_backend web_backend
# Backend with load balancingbackend web_backend balance roundrobin option httpchk http-check send meth GET uri /health http-check expect status 200 server web1 192.168.1.10:80 check server web2 192.168.1.11:80 checkThe file reads in four sections, always in this order, and knowing where a directive belongs avoids half of the configuration mistakes:
- global: settings of the HAProxy process (logs, user, limits)
- defaults: default values for frontends/backends
- frontend/listen: traffic entry points
- backend: destination server groups
Validation and startup
Before applying a configuration, always validate it:
# Checks the syntaxsudo /usr/local/sbin/haproxy -c -f /etc/haproxy/haproxy.cfgIf no error is printed, the configuration is valid. The full path of the binary is not a fad: on AlmaLinux 10, the secure_path of sudo does not contain /usr/local/sbin, and sudo haproxy answers command not found there; Debian 13 includes it.
# Start the servicesudo systemctl enable haproxysudo systemctl start haproxy
# Check the statussudo systemctl status haproxyVerification
Test that load balancing works:
# Send several requestsfor i in {1..4}; do curl -s http://localhost/ | head -1; doneYou should see the answers alternate between your backends.
Reach the statistics page, which only listens on the loopback, through an SSH tunnel: ssh -L 8404:localhost:8404 user@server, then http://localhost:8404/stats.
Advanced use cases
The basic configuration works for most static sites. But as soon as your application gets more complex, you will need advanced features. Let us look at the most common cases.
Sticky sessions (persistent sessions)
The problem: imagine an e-commerce site where the user adds items to the cart. If the first request goes to server A (which stores the cart), but the second goes to server B... the cart looks empty! Frustrating for the user.
The solution: sticky sessions guarantee that a user is always sent back to the same backend server. HAProxy uses a cookie to "remember" the server assigned to each visitor.
backend web_backend balance roundrobin cookie SERVERID insert indirect nocache server web1 192.168.1.10:80 check cookie web1 server web2 192.168.1.11:80 check cookie web2How it works:
- On the first connection, HAProxy picks a server (e.g. web1)
- It adds a cookie
SERVERID=web1to the response - The following requests carrying this cookie always go to web1
Verification:
# First request: fetch the cookiecurl -c cookies.txt http://localhost/
# Following requests: use the cookie (same server)curl -b cookies.txt http://localhost/curl -b cookies.txt http://localhost/URL-based routing (path-based routing)
The problem: your application has several components, an API, static files, WebSockets, each on different servers. How do you direct each type of request to the right server group?
The solution: use ACLs (filtering rules) to analyse the URL and route intelligently. Think of a railway switch: depending on the destination written on the ticket (the URL), the train (the request) is sent down the right track.
frontend http_front bind *:80
# ACL definitions acl is_api path_beg /api acl is_static path_beg /static /images /css /js acl is_websocket hdr(Upgrade) -i WebSocket
# Host validation (allow-list) acl valid_host hdr(host) -i example.com www.example.com api.example.com http-request deny deny_status 400 unless valid_host
# Routing to the right backends use_backend api_backend if is_api use_backend static_backend if is_static use_backend websocket_backend if is_websocket default_backend web_backendCustom HTTP health checks
The problem: a server can be "up" (the process runs) but "sick" (the database is disconnected, the disk is full...). A simple TCP check does not detect these situations.
The solution: configure HTTP health checks that call a /health endpoint of your application. This endpoint checks that everything works (DB, cache, disk space...) and returns a 200 code if OK. HAProxy automatically removes the servers that do not answer correctly.
backend web_backend balance roundrobin
# Enable HTTP health checks (HAProxy 3.x syntax) option httpchk http-check send meth GET uri /health ver HTTP/1.1 hdr Host localhost
# Expect a 200 code http-check expect status 200
# Or check the content of the response # http-check expect string "status":"healthy"
# Check parameters server web1 192.168.1.10:80 check inter 5s fall 3 rise 2 server web2 192.168.1.11:80 check inter 5s fall 3 rise 2Server parameters:
| Parameter | Description |
|---|---|
check | Enables health checks |
inter 5s | Interval between checks (5 seconds) |
fall 3 | Number of failures before the server is considered DOWN |
rise 2 | Number of successes to bring the server back UP |
SSL/TLS termination
The problem: every backend server must handle its own TLS certificate, which complicates renewal and costs CPU for encryption.
The solution: HAProxy becomes the single TLS termination point. It decrypts incoming HTTPS requests, then forwards them as plain HTTP (or HTTPS) to your backends. Benefits:
- A single certificate to manage
- Lighter backends
- Centralised renewal (Let's Encrypt, etc.)
frontend https_front bind *:443 ssl crt /etc/haproxy/certs/site.pem alpn h2,http/1.1 bind *:80
# Redirect HTTP to HTTPS http-request redirect scheme https unless { ssl_fc }
# Add the X-Forwarded-Proto header http-request set-header X-Forwarded-Proto https if { ssl_fc }
default_backend web_backendTLS baseline (recommended hardening)
Why harden TLS? Protocols and cipher algorithms evolve. What was secure in 2015 (TLS 1.0, SHA-1) is vulnerable today. The configuration below applies the 2026 good practices:
- TLS 1.2 minimum: disables the obsolete versions (1.0, 1.1)
- Modern suites: only AES-GCM and ChaCha20 (fast and secure)
- Safe elliptic curves: X25519, P-256, P-384
Add these settings to the global section of your configuration:
global # TLS: minimum version and modern suites ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
# Safe elliptic curves ssl-default-bind-curves X25519:P-256:P-384
# Same settings on the server side (if re-encrypting to HTTPS backends) ssl-default-server-options ssl-min-ver TLSv1.2 no-tls-tickets ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256If you must support old clients (IE11, Android 4.x), keep TLSv1.2 as the minimum. For 100% modern environments, you can move to ssl-min-ver TLSv1.3.
Securing HAProxy
HAProxy stands on the front line facing the Internet. It is both a risk (single entry point) and an opportunity (central control point). Let us see how to secure it.
Rate limiting
The problem: an attacker or a bot can send thousands of requests per second, saturating your servers and making the site unreachable for real users.
The solution: rate limiting counts the requests of each IP and blocks those that exceed a reasonable threshold. Think of a nightclub bouncer who turns away people who try to get in too often.
Beware: limits that are too strict can block legitimate users (several people behind the same corporate NAT). Start with conservative values and adjust according to your logs.
frontend http_front bind *:80
# Tracking table of requests per IP stick-table type ip size 100k expire 30s store http_req_rate(10s)
# Count the requests per IP http-request track-sc0 src
# Block above 50 requests in 10 seconds http-request deny deny_status 429 if { sc_http_req_rate(0) gt 50 }
default_backend web_backendExplanation:
stick-table: creates a table to store the counters per IPtrack-sc0: ties each request to its source IPdeny_status 429: returns "Too Many Requests" when the limit is exceeded
Security headers
The problem: modern browsers offer protections against clickjacking, XSS, MIME sniffing... but these protections only kick in if the server sends the right HTTP headers.
The solution: configure HAProxy to add these security headers automatically to every response. It is a quick security win, without changing your applications.
What each header does:
| Header | Protection |
|---|---|
Strict-Transport-Security | Forces HTTPS for 1 year (prevents HTTP downgrade) |
X-Frame-Options | Prevents embedding your site in an iframe (anti-clickjacking) |
X-Content-Type-Options | Prevents the browser from "guessing" the MIME type |
Content-Security-Policy | Controls which resources may be loaded |
frontend https_front bind *:443 ssl crt /etc/haproxy/certs/site.pem
# Security headers http-response set-header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" http-response set-header X-Frame-Options "SAMEORIGIN" http-response set-header X-Content-Type-Options "nosniff" http-response set-header Referrer-Policy "strict-origin-when-cross-origin" http-response set-header Permissions-Policy "geolocation=(), microphone=(), camera=()" # Basic CSP - adapt to your application http-response set-header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; frame-ancestors 'self'"
default_backend web_backendBlock malicious IPs
The problem: you spot IPs attacking your site (brute force, abusive scraping, spam). You want to block them before they reach your backends.
The solution: create a blocklist of IPs in an external file. HAProxy loads this file at startup and rejects the matching requests. Recorded on HAProxy 3.2.25: an address added to the file is not blocked until the service is reloaded. To block right away, without a reload, go through the administration socket, then copy the address into the file so that it survives the next reload:
frontend http_front bind *:80
# IP blocklist (external file) acl blocked_ips src -f /etc/haproxy/blocked_ips.txt http-request deny if blocked_ips
default_backend web_backendContent of /etc/haproxy/blocked_ips.txt:
192.168.1.10010.0.0.0/8echo "add acl /etc/haproxy/blocked_ips.txt 203.0.113.7" | sudo socat stdio /run/haproxy/admin.sockCheck that the protections work
Hardening is only worth something once tested. Here is a frontend that combines security headers, rate limiting, blocking of a sensitive path and a statistics page protected by authentication, all in front of two backends:
frontend web bind *:80 # Security headers on every response http-response set-header X-Frame-Options DENY http-response set-header X-Content-Type-Options nosniff # Rate limiting: 20 requests / 10 s per IP, otherwise 429 stick-table type ip size 100k expire 30s store http_req_rate(10s) http-request track-sc0 src http-request deny deny_status 429 if { sc_http_req_rate(0) gt 20 } # Block a sensitive path acl admin_path path_beg /admin http-request deny if admin_path default_backend servers
frontend stats bind 127.0.0.1:8404 stats enable stats uri /stats stats auth admin:a_strong_passwordEach protection is checked with curl. The headers are added to every response:
curl -sI http://localhost/ | grep -i "x-"x-frame-options: DENYx-content-type-options: nosniffThe ACL blocks /admin with a 403, without ever reaching the backend. Rate limiting lets the first requests through then returns 429 (Too Many Requests) above the threshold, and the stats page requires authentication:
GET /admin -> 40325 rapid requests -> 200 19 times, then 429 429 429 429 429 429GET /stats no auth -> 401GET /stats with auth -> 200These four checks, replayed on 30 September 2026 on HAProxy 3.2.25, from the server itself, turn a simple load balancer into a security control point at the entrance of the infrastructure. That is the whole point of centralising protection on the reverse proxy rather than scattering it across every backend.
Monitoring and logs
An infrastructure without monitoring is like driving at night without headlights. HAProxy offers several tools to see what happens in real time and diagnose problems.
Statistics page
What you see: the built-in stats page is a visual dashboard showing the state of your infrastructure at a glance. Each backend is shown with a colour code (green = UP, red = DOWN, orange = in transition).
Available information:
- Number of active and total connections
- Request rate per second
- Average backend response time
- State of each server (UP/DOWN/DRAIN)
- Error history
listen stats bind 127.0.0.1:8404 stats enable stats uri /stats stats refresh 10s stats show-legends stats show-node stats auth admin:SECURE_PASSWORDReach http://localhost:8404/stats through an SSH tunnel to see:
- The state of each backend (UP/DOWN)
- The number of active connections
- The request rates
- The response times
JSON logs
The problem: traditional text logs are hard to parse and analyse at scale. Every tool (grep, awk, Elasticsearch) has to "understand" the format.
The solution: HAProxy can format logs directly as JSON (format accepted by 3.2.25). The format is only applied if the frontend has a log destination: without log global, haproxy -c warns log format ignored. Each field is clearly identified, which eases ingestion into tools like Elasticsearch, Loki or Datadog.
defaults mode http log-format "%{+json}o %(client_ip)ci %(status)ST %(bytes)B %(duration)Ta %(backend)b %(server)s"Prometheus integration
Why Prometheus? It is the de facto standard for monitoring in Kubernetes/cloud-native environments. HAProxy can expose its metrics in Prometheus format, which lets you build Grafana dashboards and automatic alerts.
What you can monitor: active connections, requests per second, average latency, 5xx errors, backend state...
frontend prometheus bind 10.0.0.5:8405 http-request use-service prometheus-exporter if { path /metrics } no logBind this frontend to the internal address the Prometheus server reaches (here 10.0.0.5, to adapt), never to *: the metrics reveal the topology of the backends. Do not add stats enable to it, which would publish the statistics page without authentication. Then configure Prometheus to scrape http://10.0.0.5:8405/metrics.
Troubleshooting
Even with a careful configuration, problems can occur. This section helps you diagnose and solve the most frequent situations.
Common problems
How to read this table: identify the symptom you observe, then apply the matching solution.
| Symptom | Likely cause | Solution |
|---|---|---|
| "bind: cannot bind socket" | Port already in use | Check with ss -tlnp | grep 80 |
cannot bind socket (Permission denied) for [0.0.0.0:80] | HAProxy launched under a non-root account | Start the service as root, let user/group in global drop the privileges |
Unknown command: 'reload' on the socket | Command sent to admin.sock | Use the master socket (-S) or systemctl reload haproxy |
sudo haproxy: command not found (RHEL) | /usr/local/sbin missing from secure_path | Call /usr/local/sbin/haproxy by its full path |
| Server DOWN in the stats | Health check fails | Check that the /health endpoint answers 200 |
| No load balancing | Only one server UP | Check the health checks of the other servers |
| 503 error | Every server DOWN | Check network connectivity to the backends |
| Session cookie keeps changing | Wrong cookie configuration | Check the cookie parameter on each server |
Runbook commands (daily operations)
Keep this section at hand: these are the commands you will use most often in production. Beware of the reload command: the administration socket (admin.sock) refuses it with Unknown command: 'reload', it only exists on the master socket, opened by the -S option at launch.
# Full configuration validation (with warnings)sudo /usr/local/sbin/haproxy -c -V -f /etc/haproxy/haproxy.cfg
# Reload without interruption (hitless reload with seamless)sudo systemctl reload haproxy
# Manual reload through the master socket (HAProxy launched with -S /run/haproxy/master.sock)echo "reload" | sudo socat stdio /run/haproxy/master.sock
# Follow the logs in real timesudo journalctl -u haproxy -f
# Check the listening portsss -tlnp | grep haproxy
# Test a request in verbose modecurl -v http://localhost/Administration socket (runtime API)
The problem: reloading HAProxy for every small change (server maintenance, debug) can be heavy and risky in production.
The solution: the administration socket lets you interact with HAProxy in real time, without touching the configuration or reloading the service. It is like having a hidden administration console.
# Enable the socket in the global sectionglobal stats socket /run/haproxy/admin.sock mode 660 level admin stats timeout 30s
# Usage examples# See the state of the backendsecho "show stat" | sudo socat stdio /run/haproxy/admin.sock
# Temporarily disable a server (maintenance)echo "disable server web_backend/web1" | sudo socat stdio /run/haproxy/admin.sock
# Re-enable the serverecho "enable server web_backend/web1" | sudo socat stdio /run/haproxy/admin.sock
# See the active sessionsecho "show sess" | sudo socat stdio /run/haproxy/admin.sockDiagnostic mode
When to use it: your configuration is valid (haproxy -c passes) but something does not behave as expected. Diagnostic mode prints extra warnings about suspicious or obsolete directives.
sudo /usr/local/sbin/haproxy -dD -c -f /etc/haproxy/haproxy.cfgCombined with -c, the option checks the configuration while adding these warnings, without starting an instance.
Hands-on practice
Move from the configuration you read to the configuration you write: this lab has you declare a frontend and a backend of several web servers in HAProxy, then observe how traffic is spread across them on a VM.
Key points
-
HAProxy is a reverse proxy and load balancer that spreads traffic across several servers for high availability and performance.
-
The frontend/backend blocks separate receiving traffic (frontend) from distributing it (backend).
-
Health checks automatically remove failing servers from the pool: always enable them with
check. -
Sticky sessions (cookies) keep client-server affinity for stateful applications.
-
Rate limiting protects your backends against abuse: configure a
stick-tablewith reasonable limits. -
Always validate the configuration with
haproxy -cbefore reloading. -
The statistics page (port 8404) gives a real-time view of the state of your infrastructure.
Knowledge check
Check that the essentials of this guide are acquired. The questions only cover what has just been explained here.
Knowledge check
Check what you have learned with this interactive quiz
How it works
- The timer starts when you click Start
- Multiple choice, true/false and short answer questions
- You can move between questions
- Detailed results are shown at the end
Starts the quiz and the timer
Review
(0/0)Score by domain
A domain groups several guides and gets several questions, so its score is more reliable than a per-topic score, which often rests on a single question.
Summary
Strengths
No topic passed at 80% on at least two questions yet.
Weak areas
No topic below the pass mark.
Recommended next
Hints before you try again?
Continue the course: Keep a Linux server in good shape
A new full quiz with random questions
Work again only on the questions you missed
More information
The configuration shown here covers only part of the directives available. The manual of the 3.2 branch remains the source to consult as soon as a directive falls outside the scope of this guide, notably for the resolvers sections, peers and the stick-table converters.