If you've scaled a VPS as far as it'll go — more RAM, more vCPUs, tuned MySQL, added Redis, moved static assets to a CDN — and it's still the single point of failure for your app, the next step isn't a bigger box. It's more boxes. HAProxy sits in front of two or more backend VPS instances and spreads incoming traffic between them, so one server crashing or getting rebooted for maintenance doesn't take your site down with it.
This guide walks through a working HAProxy setup: install, config, health checks, session handling, and the mistakes that turn "high availability" into "high confusion" during your first failover test.
When You Actually Need a Load Balancer
Don't reach for this on a brochure site doing 500 visits a day — you're adding moving parts for a problem you don't have yet. It makes sense once any of these is true:
- A single VPS is maxing out CPU or connections during peak traffic, even after caching and query tuning
- You need zero-downtime deploys — push a new release to one backend while the other keeps serving
- Downtime has a real cost and you need failover if one VPS goes down or its host has an outage
- You're running the same app on two or more VPS instances already and manually juggling DNS
If you're not there yet, a single well-tuned VPS behind Cloudflare will outperform a poorly configured two-node cluster. Load balancing adds a database consistency problem (more on that below) that's easy to underestimate.
The Architecture
The simplest useful setup is three VPS instances:
- haproxy-01 — a small VPS (1 vCPU / 1-2GB RAM is plenty) that terminates SSL and routes traffic
- app-01 / app-02 — your existing app VPS instances, running Nginx + PHP-FPM or Node behind them
Put all three on the same private network if your provider offers one (SkyServer VPS plans support this). Traffic between HAProxy and the backends should never touch the public internet — it adds latency and an unnecessary attack surface.
Installing HAProxy
On Ubuntu/Debian:
sudo apt update
sudo apt install haproxy -y
haproxy -v
On AlmaLinux/Rocky:
sudo dnf install haproxy -y
sudo systemctl enable --now haproxy
Config lives at /etc/haproxy/haproxy.cfg. Back up the default before you touch it:
sudo cp /etc/haproxy/haproxy.cfg /etc/haproxy/haproxy.cfg.orig
A Working Base Config
Here's a minimal but production-usable frontend/backend pair. Add this to haproxy.cfg, replacing IPs with your private network addresses:
frontend http_front
bind *:80
bind *:443 ssl crt /etc/haproxy/certs/yourdomain.pem
mode http
default_backend app_servers
backend app_servers
mode http
balance leastconn
option httpchk GET /health.php
http-check expect status 200
cookie SRV insert indirect nocache
server app01 10.0.0.11:80 check cookie app01
server app02 10.0.0.12:80 check cookie app02
listen stats
bind *:8404
stats enable
stats uri /haproxy?stats
stats auth admin:ChangeThisPassword
A few things worth calling out:
- balance leastconn sends new connections to whichever backend has fewer active ones — better than plain
roundrobinfor PHP apps where request times vary a lot. - option httpchk polls a lightweight endpoint (create
health.phpthat just returns200 OKafter a quick DB ping) rather than trusting that "port 80 is open" means "app is healthy." - cookie SRV insert gives you sticky sessions — see below, this matters more than people expect.
- The stats page on port 8404 is genuinely useful for a quick "is app02 actually down or is HAProxy just being paranoid" check. Restrict it with a firewall rule, not just the basic auth line above.
Test the config syntax before reloading — a typo here takes down every backend at once:
sudo haproxy -c -f /etc/haproxy/haproxy.cfg
sudo systemctl reload haproxy
Session Persistence: The Part People Skip
If your app stores session data in PHP's default file-based sessions (/var/lib/php/sessions) and you don't configure persistence, here's what happens: a customer logs in, HAProxy sends that request to app01, the session file gets written on app01's disk. Their next click gets routed to app02 — which has never heard of that session — and they're logged out. Cart contents vanish. Checkout breaks halfway through.
The cookie SRV insert indirect line in the config above fixes this by pinning each visitor to the backend that handled their first request, via a cookie HAProxy injects. It's the fastest fix. The more robust fix — and the one you actually want for real zero-downtime deploys — is moving sessions out of local disk entirely, into Redis or your database, so any backend can serve any request. If you've already got Redis running for object caching, this is a small addition.
The Database Is Now Your Single Point of Failure
Load balancing the web tier doesn't help if both app servers write to a MySQL instance running on app01's disk. You've got two realistic options:
| Approach | Good for | Tradeoff |
|---|---|---|
| Shared/central DB VPS | Most setups — simple, one source of truth | DB becomes the new single point of failure unless it's on its own resilient VPS with backups |
| MySQL/MariaDB replication | Read-heavy apps, higher availability needs | More moving parts; write conflicts if not configured as primary/replica correctly |
For most WordPress or small-to-mid app setups, a single well-backed-up database VPS (with automated snapshots and off-site backups) is the pragmatic choice. Don't add replication until you've actually hit a wall a bigger DB VPS can't solve.
Firewall the Backends
Once HAProxy is the only thing that should talk to app01 and app02 on ports 80/443, lock that down. With firewalld:
sudo firewall-cmd --permanent --zone=public --remove-service=http
sudo firewall-cmd --permanent --zone=internal --add-source=10.0.0.5/32 --add-port=80/tcp
sudo firewall-cmd --reload
Replace 10.0.0.5 with your HAProxy node's private IP. This also stops anyone from bypassing your load balancer and hammering one backend directly.
Testing Failover Before You Need It
Don't wait for a real outage to find out your config is wrong. Stop the app on one backend and watch what happens:
sudo systemctl stop php-fpm # on app02
curl -I https://yourdomain.com
Check /haproxy?stats — app02 should flip to red/down within a couple of health-check intervals, and traffic should keep flowing through app01 without the visitor noticing. If requests start timing out instead of failing over cleanly, your timeout connect and timeout server values in the defaults section are probably too high — 5s and 30s are reasonable starting points for most web apps.
Prevention: Keeping It Boring
- Keep HAProxy config in version control (even a simple git repo on the node) so a bad edit is one
git diffaway from being caught - Alert on backend state changes, not just "server down" — Netdata or a simple cron script hitting the stats page in JSON mode (
/haproxy?stats;json) works fine - Test failover after every config change, not just once at setup
- Don't run HAProxy on the same VPS as an app backend — if that box goes down, you lose the router and a backend at once
Frequently Asked Questions
Do I still need HAProxy if I'm already using Cloudflare?
Cloudflare's proxy and load balancing products work at a different layer — mostly DNS-based routing and edge caching — and Cloudflare's own load balancing is a paid add-on with fewer options than running HAProxy yourself. Many setups use both: Cloudflare in front for CDN and DDoS protection, HAProxy behind it distributing traffic across your VPS backends.
Should I use Nginx or HAProxy for load balancing?
Nginx can do basic load balancing with upstream blocks and works fine for simple round-robin setups. HAProxy is purpose-built for it — better health checking, more balancing algorithms, and a proper stats/monitoring interface out of the box. If you're only proxying to a single backend app, stick with plain Nginx; once you're balancing across multiple VPS, HAProxy is worth the extra config.
How many backend VPS instances do I actually need?
Two is the minimum for any real redundancy — one can go down for maintenance or crash while the other keeps serving. Three lets you take one out of rotation for a deploy while still having failover coverage for the remaining two. Don't over-provision past what your traffic actually needs; it's easier to add a third backend later than to pay for idle capacity now.
Will load balancing fix a slow database or slow queries?
No — it spreads web/application load, not database load. If your slow query log is full of table scans or you're maxing out max_connections, that's a database tuning problem that adding web backends won't touch. Fix indexing and query performance first; a load balancer in front of a slow DB just means more servers waiting on the same slow queries.
Can I run HAProxy on a shared hosting cPanel account?
No — HAProxy needs root-level access to bind ports and manage its own service, which shared cPanel hosting doesn't allow. This setup requires a VPS (or a small dedicated VPS just for the HAProxy node) in front of your app backends.
