Skip to content

HAProxy Load Balancing

Applies to CentOS Stream 9 & 10 / AlmaLinux 9.x & 10.x / Rocky Linux 9.x & 10.x

HAProxy is a high-performance TCP/HTTP load balancer and reverse proxy. When a single backend server can no longer handle the traffic, or when you need to keep a service available during upgrades and outages, HAProxy distributes requests across multiple backends to achieve horizontal scaling and high availability. This guide walks you through building layer-4/layer-7 load balancing from scratch on an EL system.

  • Install HAProxy and understand the four core sections of its configuration
  • Configure a frontend and backend to distribute traffic across multiple servers
  • Use the roundrobin / leastconn scheduling algorithms with health checks
  • Enable the stats page to observe backend state
  • Handle SELinux and firewalld permissions
  • Configure TLS termination and HTTP→HTTPS redirection
  • Understand how to pair HAProxy with Keepalived so HAProxy itself is highly available
  • A system running EL 9.x or EL 10.x to act as the load balancer
  • At least two reachable backend web servers (this guide uses 10.0.0.11 / 10.0.0.12)
  • sudo privileges
Live version data by pkgseek.com

HAProxy is provided in the AppStream repository, so you can install it directly:

Install HAProxy
$ sudo dnf install haproxy

Confirm the version:

Check the version
$ haproxy -v
HAProxy version 2.4.x ...

HAProxy’s main configuration file is /etc/haproxy/haproxy.cfg, made up of four core sections:

SectionPurpose
globalProcess-level settings: logging, run-as user, max connections, tuning parameters
defaultsProvides default values for the frontend/backend sections that follow, avoiding repetition
frontendDefines the address and port to listen on, and which backend to hand traffic to
backendDefines the pool of backend servers, the scheduling algorithm, and health checks

The frontend receives client requests, the backend holds the real servers, and the two are linked by name.

The configuration below listens on port 80 and distributes HTTP requests in round-robin fashion across two backend web servers. Edit /etc/haproxy/haproxy.cfg:

/etc/haproxy/haproxy.cfg
global
log 127.0.0.1 local2
chroot /var/lib/haproxy
pidfile /var/run/haproxy.pid
maxconn 4000
user haproxy
group haproxy
stats socket /var/run/haproxy.sock mode 660 level admin
daemon
defaults
mode http
log global
option httplog
option dontlognull
retries 3
timeout connect 5s
timeout client 30s
timeout server 30s
timeout http-request 10s
frontend web_front
bind *:80
default_backend web_servers
backend web_servers
balance roundrobin
option httpchk GET /
server web1 10.0.0.11:80 check
server web2 10.0.0.12:80 check

A few key points:

  • mode http works at layer 7 (HTTP) and can parse HTTP headers; changing it to mode tcp gives a layer-4 passthrough, suitable for non-HTTP protocols like databases or SMTP.
  • balance roundrobin distributes requests in turn. If your backends have very different processing times, balance leastconn is a better fit — it sends each new connection to the backend with the fewest active connections.
  • The check in server web1 10.0.0.11:80 check enables health checking; HAProxy periodically probes the backend, removes it on failure, and adds it back once it recovers.
  • option httpchk GET / upgrades the health check from a plain TCP probe to an HTTP probe, sending a GET request to / and considering the backend healthy only on a 2xx/3xx response. In production, point it at a dedicated health-check path (such as /healthz).

HAProxy ships with a live stats page that shows the state, traffic, and session count of each backend. Add a listen section (or place it inside a frontend):

Append to /etc/haproxy/haproxy.cfg
listen stats
bind *:8404
stats enable
stats uri /haproxy?stats
stats auth admin:password
stats refresh 10s

After saving, visit http://<load-balancer-IP>:8404/haproxy?stats and log in with the username admin and password password.

After every change, validate the syntax before restarting so you do not break the service:

Validate the configuration file
$ sudo haproxy -c -f /etc/haproxy/haproxy.cfg
Configuration file is valid

If there is an error, the output points directly to the offending line number and the reason; fix it as instructed.

EL systems enable SELinux by default. HAProxy runs in the haproxy_t domain and is, by default, only allowed to connect to a set of common ports. If your backends use a non-standard port, the connection is denied by SELinux, showing up as backends that stay down even though the network itself is fine.

Turn on the boolean to allow HAProxy to connect to any port:

Allow HAProxy to connect to any backend port
$ sudo setsebool -P haproxy_connect_any on

Open the HTTP/HTTPS ports so clients can reach the load balancer:

Open 80 and 443
$ sudo firewall-cmd --permanent --add-service=http
$ sudo firewall-cmd --permanent --add-service=https
$ sudo firewall-cmd --reload

If the stats page uses 8404, open that port too:

Open the stats page port
$ sudo firewall-cmd --permanent --add-port=8404/tcp
$ sudo firewall-cmd --reload
Start and enable at boot
$ sudo systemctl enable --now haproxy

Check the status:

Check the running status
$ sudo systemctl status haproxy

From then on, after each configuration change, validate with haproxy -c -f first, then run sudo systemctl reload haproxy for a graceful reload (reload does not drop existing connections).

Let HAProxy terminate TLS directly (decrypt at the load balancer, then forward in plaintext to the backends), so clients establish an encrypted connection only with HAProxy.

  1. Prepare the certificate. HAProxy requires the full certificate chain and private key combined into a single .pem file (in the order: certificate + intermediates + private key):

    Combine fullchain and private key
    $ sudo mkdir -p /etc/haproxy/certs
    $ sudo bash -c 'cat fullchain.pem privkey.pem > /etc/haproxy/certs/site.pem'
    $ sudo chmod 600 /etc/haproxy/certs/site.pem
  2. Enable TLS on the bind line, and redirect port 80 requests to HTTPS:

    /etc/haproxy/haproxy.cfg
    frontend web_front
    bind *:80
    bind *:443 ssl crt /etc/haproxy/certs/site.pem
    # Redirect all plaintext access to HTTPS with a 301
    redirect scheme https code 301 if !{ ssl_fc }
    default_backend web_servers

    ssl_fc tests whether the current connection used TLS; !{ ssl_fc } means “plaintext request”, and redirect scheme https is applied to it.

  3. Validate and reload:

    Validate then reload
    $ sudo haproxy -c -f /etc/haproxy/haproxy.cfg
    $ sudo systemctl reload haproxy

A single HAProxy is still a single point of failure. In production, you typically deploy two HAProxy nodes and use Keepalived to float a virtual IP (VIP) between them: clients only ever reach the VIP, and when the primary node fails, the VIP automatically moves to the standby, giving the load balancer itself redundancy.

For the detailed Keepalived + VIP configuration, health-check scripts, and split-brain handling, see High Availability Basics.

This is the most common error, and it almost always means the backend has no available server: every server is down, or all health checks are failing. To investigate:

Check backend health state
$ echo "show servers state" | sudo socat stdio /var/run/haproxy.sock

This command relies on the stats socket configured in the global section above; socat is in EPEL — install it with sudo dnf install socat. A more visual approach is to open the stats page and see which servers are red (DOWN). Common causes: the backend service is not running, a firewall blocks the health check, the path in option httpchk returns a non-2xx/3xx status, or the backend uses a non-standard port that SELinux blocks (see haproxy_connect_any above).

Binding 443 Fails / Backend Connection Refused

Section titled “Binding 443 Fails / Backend Connection Refused”
  • Cannot bind 443: First confirm the certificate file exists and contains the private key — haproxy -c -f reports unable to load SSL certificate if not. Then confirm it is readable. If the log shows permission denied, it is usually an SELinux context issue; fix it with sudo restorecon -Rv /etc/haproxy/.
  • Backend connection refused: When the backend uses a non-standard port, be sure to run sudo setsebool -P haproxy_connect_any on, otherwise SELinux silently refuses the connection.

Check in order: whether the listen stats section includes stats enable; whether the firewall has opened the relevant port (such as 8404/tcp); whether the URL you visit exactly matches stats uri (here, /haproxy?stats); and whether the username and password match stats auth.

When startup fails, run this first:

Validate and locate the error
$ sudo haproxy -c -f /etc/haproxy/haproxy.cfg

The output identifies the offending line number and reason. You can also check the journal for details on the startup failure:

View the service log
$ sudo journalctl -u haproxy -n 50 --no-pager