How to Install HAProxy on Ubuntu 24.04 VPS — Load Balancer and Reverse Proxy
When a single application server starts buckling under traffic, or when you need to distribute HTTPS requests across a pool of backends without introducing a new single point of failure, HAProxy is the tool most operators reach for. It is the quietly ubiquitous edge proxy behind GitHub, Stack Overflow, Reddit, Airbnb, and a large fraction of the public internet, and a single well-tuned instance routinely sustains several million requests per second on commodity hardware. This guide walks through installing HAProxy 2.8 LTS on Ubuntu 24.04 from the official ppa:vbernat/haproxy-2.8 PPA, then configuring it as a TLS-terminating reverse proxy, a Layer 7 load balancer with health checks and ACL routing, a rate-limited public edge with stick-tables, and a monitored production service with the built-in stats page and Prometheus exporter.
Need a VPS for your HAProxy edge? The CloudCore Starter plan at EUR 7.99/month is enough to proxy a small fleet, and the Professional plan at EUR 19.99/month gives you 6 vCPU and 12 GB RAM for high-throughput production edges. Launch your VPS and follow along.
Table of Contents
What is HAProxy?
HAProxy (High Availability Proxy) is a free, open-source TCP and HTTP load balancer and reverse proxy written in C by Willy Tarreau and maintained as a Linux kernel-adjacent project since 2000. It is single-threaded per worker (with multi-thread support since 1.8), event-driven, and designed around a zero-copy forwarding path that minimizes syscalls per request. The result is a proxy that routinely handles more traffic on two cores than general-purpose web servers handle on sixteen.
Unlike an application server, HAProxy does not execute code for dynamic content, does not serve files from disk as a primary job, and does not run scripting engines. Its entire purpose is to accept inbound connections, inspect them at Layer 4 (TCP) or Layer 7 (HTTP), apply routing and safety rules, and forward them to a backend pool. This narrow focus is what lets it outperform multi-purpose tools on proxy workloads.
Typical deployments include public HTTPS edges for SaaS applications, database connection pooling in front of PostgreSQL or MySQL, TCP load balancing for Redis Sentinel and RabbitMQ clusters, API gateways with JWT-driven routing, rate-limited public endpoints, and blue/green and canary deployments where a single ACL flip shifts traffic between two versions of an application.
The project ships roughly annual LTS releases. At the time of writing, 2.8 LTS is the recommended stable branch with security support through 2028, which is why we pull from ppa:vbernat/haproxy-2.8 rather than the older version shipped in Ubuntu's default archive.
Why Run HAProxy on Your VPS?
- Predictable flat-rate throughput. A 4 vCPU VPS handles tens of thousands of HTTPS requests per second with HAProxy in front of it. You pay one monthly bill instead of per-request load-balancer charges.
- True high availability. Pair HAProxy with Keepalived and a floating virtual IP to get active/passive failover with sub-second takeover when the primary dies.
- Deep observability. The stats page, Prometheus exporter, and runtime admin socket mean you always know which backend is slow, which server is draining, and which ACL is firing.
- Layer 4 and Layer 7 in one binary. The same instance can front HTTP apps, a Postgres read-replica pool, and an SMTP cluster — each as a separate
frontend/backendblock. - Zero-downtime reloads. The master-worker model lets you push config changes with
systemctl reload haproxyand never drop a request. - Free, open-source, no vendor lock-in. Community HAProxy is the same codebase HAProxy Enterprise is built on. You can operate for years on the free version and migrate to paid support only if you want the Data Plane API, WAF, or 24/7 phone support.
HAProxy vs. Nginx vs. Traefik
Each of the three major reverse proxies has a different sweet spot. Pick based on the shape of your workload.
| Feature | HAProxy 2.8 | Nginx | Traefik |
|---|---|---|---|
| Primary purpose | Pure proxy / load balancer | Web server + proxy | Dynamic cloud-native proxy |
| Static file serving | No | Excellent | Basic |
| Raw L7 throughput | Highest | Very high | High |
| Layer 4 TCP load balancing | First-class | Via stream module | Yes |
| Dynamic service discovery | Data Plane API (extra) | Requires Plus or Lua | Native (Docker, K8s, Consul) |
| Automatic HTTPS (ACME) | Via Certbot hook | Via Certbot plugin | Native Let's Encrypt |
| Stick-table rate limiting | Best-in-class | Basic (limit_req) | Via plugin |
| Config reload downtime | Zero (SIGUSR2) | Zero (SIGHUP) | Zero (live config) |
| Stats / observability | Built-in + Prometheus | stub_status + commercial | Built-in dashboard |
| Typical use case | Public edge, DB proxy, API gateway | Web server, static + proxy, CDN origin | Docker/K8s ingress |
Prerequisites
- Ubuntu 24.04 LTS VPS with
sudoor root access. - At least 2 GB of RAM and 2 vCPU. HAProxy itself is tiny; the RAM goes to TCP buffers, SSL session cache, and stick-tables.
- Two or more backend application servers or Docker containers to load balance. For testing, two
nginxcontainers on different ports are enough. - A domain name with an
Arecord pointing to the HAProxy VPS public IP. - TLS certificate and key (Let's Encrypt via Certbot or any commercial CA).
Recommended Plan: CloudCore Starter at EUR 7.99/month>
For a small HAProxy edge fronting 2-5 backends, the CloudCore Starter plan is plenty: 4 vCPU, 8 GB RAM, 100 GB NVMe, and unmetered bandwidth for EUR 7.99/month. Scaling up to the Professional plan at EUR 19.99/month gives you 6 vCPU and 12 GB RAM for edges sustaining tens of thousands of requests per second. The Business plan starts at EUR 29.99/month when you need dedicated vCPU pinning for predictable latency.
Connect to the server:
ssh root@your-server-ipStep 1: Update the System
Refresh the package index and apply pending upgrades so the new PPA pulls against a current package set.
sudo apt update && sudo apt upgrade -y
sudo apt install -y software-properties-common ca-certificates gnupgIf the kernel was updated, reboot before continuing:
sudo rebootReconnect via SSH after a minute.
Step 2: Install HAProxy 2.8 from ppa:vbernat/haproxy-2.8
Ubuntu 24.04's default archive ships an older HAProxy that lags behind the LTS branch. Willy Tarreau's employee Vincent Bernat maintains an official PPA with current LTS builds, and it is the canonical source the HAProxy project itself links from its install docs.
Add the PPA and install:
sudo add-apt-repository -y ppa:vbernat/haproxy-2.8
sudo apt update
sudo apt install -y haproxy=2.8.\* socatExpected output (abbreviated):
Setting up haproxy (2.8.12-1ppa1~noble) ...
Created symlink /etc/systemd/system/multi-user.target.wants/haproxy.serviceVerify the installed version:
haproxy -vv | head -5Expected output:
HAProxy version 2.8.12-1ppa1~noble 2026/01/20
Running on: Linux 6.8.0-45-generic
Build options :
TARGET = linux-glibc
CPU = genericConfirm the service is enabled:
sudo systemctl status haproxyExpected output:
● haproxy.service - HAProxy Load Balancer
Loaded: loaded (/lib/systemd/system/haproxy.service; enabled; preset: enabled)
Active: active (running)The package installs the default config at /etc/haproxy/haproxy.cfg with a placeholder that listens on :80 but proxies nothing useful. We replace it entirely in Step 4.
Step 3: Understand haproxy.cfg Structure
Every HAProxy configuration is built from four section types. Recognizing them on sight makes the rest of this guide fall into place.
global— Process-level settings: the user/group HAProxy drops to, the admin socket path, SSL defaults, log target, number of threads. Oneglobalblock per file.defaults— Inherited defaults for every subsequentfrontend,backend, andlistenblock. Timeouts, default mode (httportcp), error files. You can have multipledefaultsblocks; each one resets for blocks that follow it.frontend— An entry point that binds one or more sockets and defines rules for selecting a backend. Typically "public HTTPS on :443" or "internal API on :8443".backend— A pool of one or more servers plus the algorithm, health-check rules, and per-server options for reaching them.
listen block collapses a matched frontend/backend pair into one, useful for a stats page or a TCP proxy with only one pool.Step 4: Write a Base Configuration
Back up the default file and replace it with a production-shaped skeleton.
sudo cp /etc/haproxy/haproxy.cfg /etc/haproxy/haproxy.cfg.orig sudo tee /etc/haproxy/haproxy.cfg > /dev/null <<'EOF' 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# Modern TLS defaults (Mozilla Intermediate profile) 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:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305 ssl-default-bind-options prefer-client-ciphers ssl-min-ver TLSv1.2 no-tls-tickets
defaults log global mode http option httplog option dontlognull option forwardfor option http-server-close timeout connect 5s timeout client 60s timeout server 60s timeout http-request 10s timeout http-keep-alive 10s retries 3
frontend http_in bind *:80 # Redirect everything to HTTPS (we add the 443 bind in Step 5) http-request redirect scheme https code 301 unless { ssl_fc }
backend app_pool balance roundrobin option httpchk GET /health http-check expect status 200 default-server inter 3s fall 3 rise 2 server app1 10.0.0.11:8080 check server app2 10.0.0.12:8080 check EOF
Replace 10.0.0.11 and 10.0.0.12 with the private IPs of your backend application servers. Each server line takes the form server <name> <ip>:<port> [options], and the check keyword activates the health check defined by option httpchk.
Validate the configuration before reloading — a syntax error here takes down the proxy:
sudo haproxy -c -f /etc/haproxy/haproxy.cfgExpected output:
Configuration file is validReload the service:
sudo systemctl reload haproxyStep 5: Enable TLS Termination with HTTP/2 and ALPN
HAProxy expects a single PEM file containing the private key followed by the full certificate chain. If you obtained a cert from Let's Encrypt with Certbot, combine the files:
sudo mkdir -p /etc/haproxy/certs
sudo bash -c '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
sudo chown haproxy:haproxy /etc/haproxy/certs/example.com.pemAdd an HTTPS frontend. Extend /etc/haproxy/haproxy.cfg by replacing the frontend http_in block with:
frontend http_in bind *:80 http-request redirect scheme https code 301 unless { ssl_fc }
frontend https_in bind *:443 ssl crt /etc/haproxy/certs/example.com.pem alpn h2,http/1.1 http-response set-header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" http-response set-header X-Content-Type-Options nosniff default_backend app_pool
The important flags on the bind line:
ssl— Activates TLS termination on this socket.crt /etc/haproxy/certs/example.com.pem— Path to the combined key+chain PEM. Can also be a directory; HAProxy loads every PEM inside and picks the right one via SNI.alpn h2,http/1.1— Advertises HTTP/2 (h2) first with HTTP/1.1 fallback during the TLS handshake via ALPN. Modern browsers negotiate HTTP/2 automatically.
/etc/letsencrypt/renewal-hooks/deploy/haproxy.sh:sudo tee /etc/letsencrypt/renewal-hooks/deploy/haproxy.sh > /dev/null <<'EOF'
#!/bin/bash
set -e
cat /etc/letsencrypt/live/example.com/fullchain.pem \
/etc/letsencrypt/live/example.com/privkey.pem \
> /etc/haproxy/certs/example.com.pem
chmod 600 /etc/haproxy/certs/example.com.pem
chown haproxy:haproxy /etc/haproxy/certs/example.com.pem
systemctl reload haproxy
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/haproxy.shValidate and reload:
sudo haproxy -c -f /etc/haproxy/haproxy.cfg
sudo systemctl reload haproxyConfirm HTTP/2 works:
curl -sI --http2 https://example.com/ | head -1Expected output:
HTTP/2 200Step 6: Route Traffic with ACLs
ACLs (Access Control Lists) are named boolean expressions that HAProxy evaluates on every request. Combine them with use_backend to route traffic anywhere.
Extend the https_in frontend:
frontend https_in bind *:443 ssl crt /etc/haproxy/certs/example.com.pem alpn h2,http/1.1 http-response set-header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"# Named ACLs acl host_api hdr(host) -i api.example.com acl host_admin hdr(host) -i admin.example.com acl path_static path_beg /static/ acl path_websocket path_beg /ws/ acl method_post method POST
# Routing use_backend api_pool if host_api use_backend admin_pool if host_admin use_backend static_pool if path_static use_backend websocket_pool if path_websocket default_backend app_pool
backend api_pool balance roundrobin option httpchk GET /health server api1 10.0.0.21:8080 check server api2 10.0.0.22:8080 check
backend admin_pool balance roundrobin server admin1 10.0.0.31:8080 check
backend static_pool balance uri hash-type consistent server cache1 10.0.0.41:80 check server cache2 10.0.0.42:80 check
backend websocket_pool balance leastconn timeout tunnel 1h server ws1 10.0.0.51:8080 check server ws2 10.0.0.52:8080 check
Five rules, five common patterns. Host-based virtual hosting (host_api, host_admin), path-based routing (path_static), protocol-based routing (path_websocket with a one-hour tunnel timeout for long-lived connections), and a fallthrough default_backend catch-all.
Other useful ACL sources:
src— Source IP, e.g.acl internal_network src 10.0.0.0/8 192.168.0.0/16.ssl_fc_sni— The SNI hostname from the TLS handshake, which works even before any HTTP parsing.req.hdr(authorization) -m found— Matches whether a header exists.url_param(token) -m found— True if the query string contains?token=....
sudo haproxy -c -f /etc/haproxy/haproxy.cfg && sudo systemctl reload haproxyStep 7: Pick a Load-Balancing Algorithm
The balance directive in each backend selects how requests are distributed across servers. Four algorithms cover nearly every real workload.
| Algorithm | How it works | Best for |
|---|---|---|
roundrobin | Cycles evenly through the server list, weighted by weight | Stateless HTTP APIs, uniform backends |
leastconn | Picks the server with fewest active connections | WebSockets, DB proxies, long-lived requests |
uri | Hashes the URL path to pick a server (consistent with hash-type consistent) | Cache servers, content origins with locality |
source | Hashes the client IP to a server | Sticky sessions without cookies |
backend api_pool balance roundrobin server api1 10.0.0.21:8080 check weight 100 server api2 10.0.0.22:8080 check weight 100backend websocket_pool balance leastconn timeout tunnel 1h server ws1 10.0.0.51:8080 check
backend static_pool balance uri hash-type consistent server cache1 10.0.0.41:80 check server cache2 10.0.0.42:80 check
For cookie-based session persistence, add inside a backend:
cookie SERVERID insert indirect nocache
server app1 10.0.0.11:8080 check cookie app1
server app2 10.0.0.12:8080 check cookie app2HAProxy now injects a SERVERID=app1 cookie on the first response and pins the client to that server until the cookie expires.
Step 8: Rate Limit with Stick-Tables
Stick-tables are HAProxy's in-memory key-value store, built for per-source-IP counters that are updated and queried at line rate. They are the mechanism behind rate limiting, brute-force detection, and DDoS mitigation.
Add a stick-table to track HTTP request rate per source IP over 10 seconds, plus an ACL that rejects offenders:
frontend https_in bind *:443 ssl crt /etc/haproxy/certs/example.com.pem alpn h2,http/1.1# Track each source IP's HTTP request rate over 10s, keep entries for 10m stick-table type ip size 1m expire 10m store http_req_rate(10s),conn_cur
# Count every request against this source IP http-request track-sc0 src
# Reject if more than 100 requests in the last 10 seconds http-request deny deny_status 429 if { sc_http_req_rate(0) gt 100 }
# Or return a slower-down for less aggressive abuse http-request tarpit if { sc_http_req_rate(0) gt 50 }
default_backend app_pool
Three primitives are at work:
stick-table— Declares the table with a key type (ip), size (1 million entries), TTL (expire 10m), and counters to maintain (http_req_rate(10s)= requests in the last 10 seconds;conn_cur= current concurrent connections).track-sc0 src— Assigns slot 0 of the sticky counters to the source IP. Any later rule can referencesc_http_req_rate(0)to read the tracked value.http-request deny/tarpit— Two responses:denyreturns immediately with a 429;tarpitholds the connection fortimeout tarpit(default 1s) before closing, which is effective against brute-force login attempts because it slows attackers without rewarding retries.
listen block:listen postgres_fe
bind *:5432
mode tcp
stick-table type ip size 100k expire 1m store conn_rate(10s)
tcp-request connection track-sc0 src
tcp-request connection reject if { sc_conn_rate(0) gt 20 }
default_backend postgres_poolStep 9: Enable the Stats Page and Prometheus Exporter
HAProxy ships with two built-in monitoring interfaces. Enable both on a private port behind your firewall.
Append to /etc/haproxy/haproxy.cfg:
frontend stats bind 127.0.0.1:8404 mode http stats enable stats uri /stats stats refresh 10s stats admin if LOCALHOST stats auth admin:ChangeThisStrongPassword
# Prometheus scrape endpoint http-request use-service prometheus-exporter if { path /metrics } no log
This single frontend block does three things:
http://127.0.0.1:8404/stats — a live HTML dashboard of every backend, server, queue depth, response time, and bytes/second. Auth-protected; admin actions (disable/enable servers, reset counters) only allowed from localhost.http://127.0.0.1:8404/metrics — native Prometheus text format with all the metrics the stats page shows plus more. No external exporter needed; this is built into HAProxy since 2.0.no log directive keeps scrapes out of your main access log.Reload and test from the server itself:
sudo systemctl reload haproxy
curl -u admin:ChangeThisStrongPassword http://127.0.0.1:8404/stats | head
curl http://127.0.0.1:8404/metrics | headThen point your Prometheus scrape job at http://haproxy-host:8404/metrics and import the HAProxy 2 Grafana dashboard for instant visualization.
To expose the stats page remotely, tunnel it over SSH rather than opening port 8404:
ssh -L 8404:127.0.0.1:8404 user@haproxy-hostThen open http://localhost:8404/stats in your browser.
Step 10: Use the Admin Socket with socat
The stats socket directive in global exposes a Unix domain socket at /run/haproxy/admin.sock. Talking to it with socat lets you change HAProxy behavior live — without editing the config file, without reloading, and without dropping a single connection.
Install socat (done in Step 2) and verify the socket exists:
ls -l /run/haproxy/admin.sockExpected output:
srw-rw---- 1 haproxy haproxy 0 Apr 16 10:00 /run/haproxy/admin.sockCommon admin commands:
# Show the current server table
echo "show servers state" | sudo socat stdio /run/haproxy/admin.sockDrain a server for maintenance (stop sending new requests, let existing ones finish)
echo "set server app_pool/app1 state drain" | sudo socat stdio /run/haproxy/admin.sockDisable it completely
echo "disable server app_pool/app1" | sudo socat stdio /run/haproxy/admin.sockBring it back
echo "enable server app_pool/app1" | sudo socat stdio /run/haproxy/admin.sockChange its weight live (0-256)
echo "set weight app_pool/app1 50" | sudo socat stdio /run/haproxy/admin.sockInspect a stick-table
echo "show table https_in" | sudo socat stdio /run/haproxy/admin.sockKick a specific IP out of the stick-table
echo "clear table https_in key 203.0.113.99" | sudo socat stdio /run/haproxy/admin.sockReset all stats counters
echo "clear counters all" | sudo socat stdio /run/haproxy/admin.sockShow HAProxy info (uptime, PID, threads, connection count)
echo "show info" | sudo socat stdio /run/haproxy/admin.sockFor scripted graceful deploys, the canonical drain-and-replace pattern is:
# Pre-deploy: drain app1 echo "set server app_pool/app1 state drain" | sudo socat stdio /run/haproxy/admin.sock sleep 30 # let in-flight requests finishDeploy your new code on app1
...
Post-deploy: bring it back, drain app2
echo "enable server app_pool/app1" | sudo socat stdio /run/haproxy/admin.sock echo "set server app_pool/app2 state drain" | sudo socat stdio /run/haproxy/admin.sock
etc.
No config changes, no reloads, no dropped connections — the foundation of every zero-downtime deploy pipeline built around HAProxy.
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
[ALERT] 0:0/0 : parsing [haproxy.cfg:N] on reload | Syntax error at line N | Always run sudo haproxy -c -f /etc/haproxy/haproxy.cfg before reloading. Fix the reported line. |
503 Service Unavailable from HAProxy | All servers in the backend are marked DOWN by health checks | echo "show servers state" \</td><td>sudo socat stdio /run/haproxy/admin.sock<code> to see the failure reason. Verify the </code>option httpchk<code> path returns 200 from the backend directly with </code>curl. |
| TLS handshake fails with unknown protocol | Cert PEM is missing the private key or the order is wrong | The combined PEM must be fullchain.pem followed by privkey.pem (or vice versa — HAProxy accepts either). Run openssl x509 -in /etc/haproxy/certs/example.com.pem -noout -subject to confirm the cert loads. |
cannot bind socket [0.0.0.0:443] | Another process already owns port 443 | sudo ss -ltnp sport = :443 to find it. Common culprits: leftover Nginx, Apache, Caddy. Stop that service first. |
| Stats page shows all backends DOWN right after install | option httpchk GET /health hits an endpoint that does not exist on your app | Either implement a /health endpoint or change to option httpchk GET /. |
| Reloads cause 1-2 second spike of connection errors | systemd is using reload instead of reload-type mixed | On 2.8 LTS this is already correct. Confirm with systemctl show haproxy \</td><td>grep ExecReload<code> — it should include </code>-sf. |
| WebSockets disconnect after 60 seconds | Default timeout server is too short | Add timeout tunnel 1h to the WebSocket backend (see Step 6 example). |
| Stick-table rate limit never triggers | Forgot http-request track-sc0 src before the deny rule | The sc_* fetch functions only work on tracked counters. Re-check order in the frontend. |
Viewing Logs
HAProxy logs to rsyslog via /dev/log in our config. View recent entries with:
sudo journalctl -u haproxy -fOr, if rsyslog is configured (Ubuntu's default), tail /var/log/haproxy.log:
sudo tail -f /var/log/haproxy.logEach HTTP log line includes the client IP, the backend chosen, the server picked, connection timers (Tq/Tw/Tc/Tr/Tt — queue, wait, connect, response, total in milliseconds), the status code, and the bytes read. Reading the timers is how you locate slow backends versus slow clients.
FAQ
Is HAProxy faster than Nginx for load balancing?
For pure Layer 4 and Layer 7 HTTP load balancing without static content, HAProxy is typically measurably faster and uses less CPU per request than Nginx at the same throughput. HAProxy was designed from day one as a proxy; Nginx is primarily a web server with a proxy module bolted on. Published benchmarks put HAProxy in the 2-3 million requests per second range on modern hardware with 10-40 percent less CPU time than Nginx for the same workload. However, Nginx wins the moment you need to serve static assets, run Lua modules, or terminate complex WebDAV/HTTP caching, because it can do all of that in one process. A common production shape is HAProxy at the edge and Nginx or Caddy as the per-app web server behind it.
Should I use HAProxy or Traefik for a Kubernetes cluster?
Traefik is the better fit if you want zero-config dynamic discovery from Docker, Kubernetes, or Consul — it watches the orchestrator's API and reconfigures itself live with no reload. HAProxy needs either the HAProxy Kubernetes Ingress Controller or the Data Plane API to achieve the same, which adds moving parts. HAProxy wins when raw throughput, deep TCP/Layer 4 control, or advanced ACL and stick-table features matter more than dynamic discovery. A common pattern is Traefik inside the cluster for service mesh routing and HAProxy at the edge as the external load balancer fronting the cluster nodes.
Can HAProxy generate Let's Encrypt certificates automatically?
HAProxy does not include native ACME/Let's Encrypt automation in the community edition. You have three practical options: run Certbot alongside HAProxy and reload it via a --deploy-hook (the approach in Step 5); use acme.sh with its HAProxy deploy plugin; or put Caddy on port 80/443 in front of HAProxy and let Caddy handle HTTPS while forwarding to HAProxy on an internal port. The Data Plane API in HAProxy Enterprise adds native ACME, but on the free tier the Certbot-with-deploy-hook pattern is cleanest and every bit as reliable.
How do I do a zero-downtime HAProxy reload?
Use systemctl reload haproxy, which sends SIGUSR2 to the master process. HAProxy spawns a new worker with the updated configuration, the old worker finishes draining its in-flight connections, then the old worker exits. No requests are dropped. For routine changes like enabling/disabling a single server, skip reload entirely by writing to the admin socket: echo 'disable server web/srv1' | sudo socat stdio /run/haproxy/admin.sock. Socket commands are instant and survive across reloads if you persist state, which the vbernat PPA package does automatically via -x /run/haproxy/admin.sock in the systemd unit.
What is the difference between roundrobin, leastconn, and uri hashing?
roundrobin cycles evenly through healthy servers and is perfect for stateless HTTP APIs where every server is equivalent. leastconn sends each new connection to the server with the fewest active connections, which works far better for long-lived connections like WebSockets, database proxies, and requests with variable duration (one slow query can snowball with roundrobin but self-balances with leastconn). uri hashing pins requests for the same URL to the same server, which maximizes cache hit rates in front of a fleet of cache servers like Varnish or a content origin benefiting from hot-file locality. Combine balance uri with hash-type consistent to minimize re-hashing when a server is added or removed. You can mix algorithms across backends in the same haproxy.cfg — the algorithm is per-backend.
Does HAProxy support HTTP/3 (QUIC)?
HAProxy added HTTP/3-over-QUIC support experimentally in 2.6 and it is production-ready in 2.8 LTS when the binary is built against QUICTLS or the OpenSSL 3.2 QUIC API. On the ppa:vbernat/haproxy-2.8 build used in this guide, HTTP/3 is available but off by default. Enable it by adding a quic4 bind alongside the TCP bind:
frontend https_in
bind *:443 ssl crt /etc/haproxy/certs/example.com.pem alpn h2,http/1.1
bind quic4@:443 ssl crt /etc/haproxy/certs/example.com.pem alpn h3
http-response set-header Alt-Svc "h3=\":443\"; ma=86400"The Alt-Svc header tells compliant clients (Chrome, Firefox) to retry over HTTP/3 on the next request. HTTP/2 over TCP via ALPN is enabled out of the box in Step 5; HTTP/3 is a strictly opt-in extension once you are ready to open UDP/443 on your firewall.
How much traffic can one HAProxy instance handle?
On a modern 4 vCPU VPS, a plain HTTP load balancer doing roundrobin with health checks sustains roughly 200,000 HTTP requests per second. With TLS termination on the same hardware, that drops to 30,000-50,000 HTTPS requests per second depending on cipher suite (AES-GCM with AES-NI hardware acceleration is fast; ChaCha20 without AES-NI is slower). For reference, the HAProxy team has demonstrated 2 million HTTPS requests per second on a 64-core server. The first resource to run out is almost always RAM for TLS session cache at very high connection churn, not CPU; set tune.ssl.cachesize appropriately. For multi-million-request edges, scale horizontally with Keepalived and a floating IP rather than vertically on a single box.
Next Steps
- Add active/passive HA with Keepalived. Pair two HAProxy VPS instances and a floating virtual IP so a single hardware failure does not take your edge down. See How to Install Keepalived on Ubuntu for the VRRP setup.
- Run Nginx or Caddy behind HAProxy for static content. Let HAProxy handle TLS and routing at the edge while Nginx or Caddy serves static assets from memory on each app server. HAProxy's
option forwardforalready injects the real client IP so your backend logs are accurate.
- Monitor with Prometheus + Grafana. Point Prometheus at the
/metricsendpoint from Step 9 and import Grafana dashboard 12693 for a live view of every backend, response time percentile, and 5xx rate.
- Layer a WAF in front. For public-facing edges, drop ModSecurity or Coraza in front of HAProxy, or use the HAProxy SPOE (Stream Processing Offload Engine) to offload request inspection to a dedicated worker. The official
haproxy-spoa-modsecurityagent integrates in under 10 minutes.
- Automate deploys with the admin socket. Build your CI/CD pipeline around
socatdrain commands so every deploy runs throughdrain -> wait -> deploy -> enable -> next server. Your users never see a 502.
Launch the VPS for your HAProxy Edge>
- CloudCore Starter — EUR 7.99/month: 4 vCPU, 8 GB RAM, 100 GB NVMe. Ideal for a single-edge HAProxy fronting 2-5 backends.
- CloudCore Professional — EUR 19.99/month: 6 vCPU, 12 GB RAM, 100 GB NVMe. Production edges sustaining tens of thousands of HTTPS requests per second.
- CloudCore Business — from EUR 29.99/month: Dedicated vCPU pinning and predictable latency for mission-critical edges.>
Deploy your HAProxy VPS now and have the Ubuntu 24.04 base ready in under 60 seconds.