
September 10, 2026
12 min read
By Kokil Thapa | Last reviewed: September 2026
Your Laravel app outgrows a single server, and traffic spikes during Dashain sales or booking surges on a travel portal. HAProxy Load Balancing: Config and TLS is the layer that keeps requests flowing when one PHP-FPM node fails or a deploy goes wrong. I use HAProxy on Linux production stacks when a client needs predictable HTTP routing, fast health checks, and TLS termination without bolting on a full reverse-proxy stack they cannot maintain. This guide walks through a working config you can deploy on Ubuntu 24 with PHP 8.4 or Laravel 12 backends behind it.
frontend on port 443 with a PEM certificate, a backend pool of app servers, option httpchk health checks, and balance roundrobin or least-conn routing—then reload safely after validating syntax with haproxy -c.What is HAProxy load balancing and when should you use it?
HAProxy is a dedicated layer-4 and layer-7 load balancer. It accepts client connections, picks a healthy backend server, and forwards the request. Unlike Nginx used as a general web server, HAProxy focuses on routing, queuing, and observability.
On real client projects I reach for HAProxy when the team wants one stable entry point for multiple app nodes. A common pattern: two Ubuntu servers running Apache plus PHP-FPM 8.4, with HAProxy on a small third instance or co-located on each node in a pair. For custom Laravel applications, that front door also centralises TLS, rate limits, and request logging.
HAProxy fits well when you need:
- Sub-millisecond health checks without full HTTP page loads
- Clear stats at
/statsfor ops teams with limited Kubernetes experience - TLS termination before traffic hits slower PHP workers
- Graceful drain during zero-downtime deploys with Deployer symlink swaps
- HTTP/2 or HTTP/3 at the edge while backends stay on plain HTTP
When HAProxy is the wrong tool, say so early. A single small WordPress site on shared hosting does not need it. A three-node Laravel cluster with scheduled deploys and payment webhooks does.
| Criteria | HAProxy | Nginx as LB | Cloud LB (ALB/NLB) |
|---|---|---|---|
| Health check speed | Excellent, native TCP/HTTP | Good | Good, vendor-managed |
| TLS termination | Strong, SNI, ALPN | Strong | Managed certs |
| Static file serving | No — use app or CDN | Yes | Varies |
| Cost on own VPS | Free, low RAM | Free | Monthly fee per LB |
| Ops on small Nepal teams | One config file | Familiar if already used | Less server access |
| Best fit | Dedicated LB tier | LB + static in one box | Cloud-native scale |
For deeper Nginx comparisons, see the Nginx vs Apache performance guide. HAProxy often sits in front of either.
How do you install and configure HAProxy for HTTP load balancing?
Start with a supported package on Ubuntu 24.04. HAProxy 2.8 or 2.9 is typical in 2026 repos. Pin the version in your runbook so staging matches production.
Install HAProxy on Ubuntu
sudo apt update
sudo apt install -y haproxy
haproxy -v
sudo systemctl enable haproxy The main config lives at /etc/haproxy/haproxy.cfg. Back it up before edits. Validate every change:
sudo haproxy -c -f /etc/haproxy/haproxy.cfg
sudo systemctl reload haproxy Minimal global and defaults block
These settings apply to every frontend and backend. Tune maxconn to your RAM and expected concurrent users.
global
log /dev/log local0
maxconn 4096
user haproxy
group haproxy
daemon
defaults
log global
mode http
option httplog
option dontlognull
timeout connect 5s
timeout client 50s
timeout server 50s
option forwardfor
option http-server-close Define frontend and backend pools
A frontend binds to a port. A backend lists real servers. This example balances three Laravel nodes on internal port 8080.
frontend http_front
bind *:80
redirect scheme https code 301 if !{ ssl_fc }
frontend https_front
bind *:443 ssl crt /etc/haproxy/certs/site.pem alpn h2,http/1.1
default_backend laravel_nodes
backend laravel_nodes
balance roundrobin
option httpchk GET /health HTTP/1.1\r\nHost:\ site.example\r\n
http-check expect status 200
server app1 10.0.1.11:8080 check inter 3s fall 3 rise 2
server app2 10.0.1.12:8080 check inter 3s fall 3 rise 2
server app3 10.0.1.13:8080 check inter 3s fall 3 rise 2 Your Laravel app should expose a cheap /health route. It can return JSON and skip database hits unless you want deep checks. On booking platforms I have shipped, a lightweight health endpoint saved us from routing to nodes mid-deploy.
Balance algorithms that matter in production
- roundrobin — even rotation; fine for homogeneous Laravel nodes
- leastconn — sends to the node with fewest active connections; better for long-polling or Livewire
- source — hash by client IP; crude session affinity when cookies are not available
- uri — hash by URL path; useful for cache-friendly read-heavy APIs
For REST API backends, I often pair leastconn with short PHP-FPM timeouts. Payment callback paths may need sticky sessions instead.
How do you set up TLS termination with HAProxy?
TLS at HAProxy means the load balancer holds the certificate. Backend nodes run plain HTTP on a private network. That keeps cert renewals in one place and reduces CPU load on app servers.
Build a PEM bundle for HAProxy
HAProxy expects cert and key in one file. Concatenate full chain plus private key:
sudo mkdir -p /etc/haproxy/certs
sudo cat /etc/letsencrypt/live/site.example/fullchain.pem \
/etc/letsencrypt/live/site.example/privkey.pem \
| sudo tee /etc/haproxy/certs/site.pem > /dev/null
sudo chmod 640 /etc/haproxy/certs/site.pem
sudo chown root:haproxy /etc/haproxy/certs/site.pem Renew with Certbot, then reload HAProxy. Automate via a deploy hook:
#!/bin/bash
cat /etc/letsencrypt/live/site.example/fullchain.pem \
/etc/letsencrypt/live/site.example/privkey.pem \
> /etc/haproxy/certs/site.pem
systemctl reload haproxy Official guidance lives in the HAProxy 2.8 documentation. For cipher hardening, cross-check the Mozilla SSL Configuration Generator.
Enable TLS 1.2 and TLS 1.3 with sane ciphers
Match your compliance needs. A practical 2026 baseline:
bind *:443 ssl crt /etc/haproxy/certs/site.pem alpn h2,http/1.1
ssl-min-ver TLSv1.2
ssl-max-ver TLSv1.3
ciphers ECDHE+AESGCM:ECDHE+CHACHA20:!aNULL:!MD5:!DSS
ssl-prefer-server-ciphers off Read the TLS 1.3 vs 1.2 configuration guide for cipher rationale. The same trade-offs apply here.
Pass client IP and scheme to Laravel
Laravel must trust HAProxy so TrustProxies reads correct HTTPS URLs. Set headers in HAProxy:
frontend https_front
bind *:443 ssl crt /etc/haproxy/certs/site.pem
http-request set-header X-Forwarded-Proto https
http-request set-header X-Forwarded-Port 443
default_backend laravel_nodes In Laravel 12, configure trusted proxies in bootstrap/app.php or middleware. Wrong trust settings break signed URLs, Sanctum cookies, and eSewa return URLs on Nepali payment flows.
Multi-domain SNI with one HAProxy instance
Bind multiple PEM files for law-firm sites sharing one LB:
bind *:443 ssl crt /etc/haproxy/certs/ alpn h2,http/1.1
acl host_notary hdr(host) -i notary.example.com
acl host_law hdr(host) -i law.example.com
use_backend notary_pool if host_notary
use_backend law_pool if host_law I have used this pattern on sister legal-tech sites that share EC2 but keep separate TLS identities. It beats running three separate load balancers at Rs 3,000–5,000/month (~USD 22–37) each.
How do you enable health checks, sticky sessions, and safe deploys?
Health checks separate a load balancer demo from production reliability. Session persistence matters when PHP stores sessions on local disk instead of Redis.
Layer-7 checks vs TCP checks
TCP checks only verify a port is open. HTTP checks validate your app responds correctly. For Laravel, always prefer HTTP:
option httpchk GET /health HTTP/1.1\r\nHost:\ site.example\r\n
http-check expect status 200
http-check expect string ok During deploys, mark a server in maintenance before stopping PHP-FPM:
echo "set server laravel_nodes/app1 state maint" | sudo socat stdio /run/haproxy/admin.sock
/* deploy */
echo "set server laravel_nodes/app1 state ready" | sudo socat stdio /run/haproxy/admin.sock Enable the stats socket in global for runtime changes:
stats socket /run/haproxy/admin.sock mode 660 level admin
stats timeout 30s Cookie-based sticky sessions
When Redis is not yet in the stack, stick users to one node:
backend laravel_nodes
balance roundrobin
cookie SERVERID insert indirect nocache
server app1 10.0.1.11:8080 check cookie app1
server app2 10.0.1.12:8080 check cookie app2 Sticky sessions hide broken session storage. Fix the root cause with Redis session and cache config when traffic grows.
Rate limiting abusive traffic
HAProxy can throttle before requests hit PHP. Track client IPs in a stick table:
frontend https_front
stick-table type ip size 100k expire 30s store http_req_rate(10s)
acl abuse table_http_req_rate(https_front) gt 100
http-request deny deny_status 429 if abuse Pair this with app-level limits on API rate limiting for defence in depth.
How do you monitor, load-test, and troubleshoot HAProxy in production?
If you cannot see backend state, you are guessing. HAProxy ships a built-in stats UI and structured logs.
Enable stats and Prometheus export
listen stats
bind *:8404
stats enable
stats uri /stats
stats refresh 10s
stats admin if LOCALHOST Restrict port 8404 with UFW. Never expose it publicly without auth. For deeper dashboards, pair with Netdata server monitoring or export via the Prometheus module.
Load-test through HAProxy, not around it
Run k6 against the public hostname so TLS and routing are in the path:
k6 run --vus 50 --duration 5m script.js See the load testing with k6 guide and the PHP-focused variant for Laravel endpoints. Watch HAProxy stats during the run. Rising queue depth means you need more nodes or lower PHP-FPM pm.max_children per box.
Typical production failures
- 502 Bad Gateway — all backends down or wrong port; check
systemctl status haproxyand app logs - SSL handshake errors — expired PEM, wrong permissions, missing intermediate chain
- Redirect loops — Laravel thinks request is HTTP; fix
X-Forwarded-Protoand trusted proxies - Uneven load — sticky cookies or one slow node; try
leastconn - Cert renewals fail — hook did not rebuild PEM; HAProxy still serves old file until reload
Validate config syntax before every reload. A broken config can drop every site behind the LB. Use regex tester tools when debugging ACL patterns, and JSON formatter utilities when health endpoints return structured payloads.
For Kubernetes workloads, ingress controllers often replace HAProxy. The concepts overlap with Kubernetes ingress and TLS with cert-manager. On bare VPS clusters common in Nepal, HAProxy stays the simpler choice.
Key Takeaways
- Terminate TLS at HAProxy with a combined PEM file and reload after every Certbot renewal.
- Always use HTTP health checks against a real Laravel
/healthroute, not bare TCP port checks. - Set
X-Forwarded-Protoand configure Laravel trusted proxies so URLs and payments stay correct. - Drain nodes to MAINT via the admin socket before deploys; return to READY only after opcache-safe PHP-FPM reload.
- Load-test through the public hostname with k6 so HAProxy TLS and balancing sit in the critical path.
- Lock port 8404 stats and admin sockets behind localhost or VPN — exposed stats panels are an easy target.
People Also Ask
Does HAProxy support TLS 1.3 in 2026?
Yes. HAProxy 2.6 and later supports TLS 1.3 when built against OpenSSL 1.1.1 or newer. Set ssl-min-ver TLSv1.2 and ssl-max-ver TLSv1.3 on your bind line. Use ALPN h2,http/1.1 if you serve HTTP/2 to browsers.
Should PHP apps sit behind HAProxy or Nginx?
Use HAProxy as the dedicated load balancer and keep Nginx or Apache on app nodes for PHP-FPM. Nginx alone can load-balance, but HAProxy gives faster health checks and cleaner ops at scale. Single-server sites rarely need both.
Where do Let's Encrypt certificates live for HAProxy?
Certbot stores certs under /etc/letsencrypt/live/. HAProxy needs cert plus key merged into one PEM under /etc/haproxy/certs/. Automate the merge in a Certbot deploy hook, then reload HAProxy.
How do you debug uneven load across backends?
Check the stats page for active sessions per server. Sticky cookies, source-based balancing, or one slow node skew traffic. Switch to leastconn, remove stickiness if sessions live in Redis, and confirm all nodes run the same PHP-FPM pool sizes.
Ship HAProxy with confidence
HAProxy Load Balancing: Config and TLS is not exotic infrastructure. It is a single well-tested config file that keeps your Laravel or WordPress cluster online when a node fails or you deploy at midnight Nepal time. Start with two app servers, HTTP health checks, and automated PEM reloads. Add sticky sessions and rate limits only when metrics prove you need them.
If you want this wired into your stack — load balancer, hosting setup, Deployer pipeline, and post-launch support — review the Notary Nepal portfolio case or browse more work on the portfolio page. For performance tuning after go-live, see speed optimization services and testing and optimization. Need a second pair of eyes on ACL rules or cipher suites? Contact us with your current haproxy.cfg and backend count.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

