HAProxy’s config file is structured around four kinds of sections — global, defaults, frontend, and backend — plus a handful of optional ones (listen, peers, resolvers, userlist). Understanding what belongs where is the whole game; almost every “why isn’t my rule firing” bug traces back to a directive in the wrong section. This article walks through the layout with a real config you can adapt for a small site.
How to verify
Start from whatever ships at /etc/haproxy/haproxy.cfg and look at the section headers:
grep -nE '^(global|defaults|frontend|backend|listen|peers)' /etc/haproxy/haproxy.cfg
sudo haproxy -c -f /etc/haproxy/haproxy.cfg
sudo haproxy -vv | head -5
ss -ltnp | grep haproxy
The grep line gives you the table of contents — if you see a frontend with no matching backend, the config will refuse to load. The -c flag is the configtest; it catches typos, missing referenced backends, and unparseable values before you reload.
What’s happening
Each section type has a specific job:
global— process-wide settings: user, group, chroot, max connections, log target, SSL defaults. Loaded once at startup, not per-request.defaults— values inherited by every followingfrontend/backend/listenunless overridden. You can have multipledefaultsblocks; each one applies to sections that follow it until anotherdefaultsappears.frontend— what HAProxy listens on. Defines bind addresses, the mode (httportcp), ACLs, and which backend a request goes to.backend— where requests go. Defines the server pool, health checks, balance algorithm, and per-server tuning.listen— a frontend and backend collapsed into one section. Convenient for simple TCP services where you do not need separate ACL logic; we avoid it in production HTTP setups because it makes content switching impossible.
A request flows: client → frontend (bind, ACLs, use_backend) → backend (server line picked by balance algorithm) → real server. The mode (http vs tcp) is set in defaults or per-section; in HTTP mode HAProxy parses requests and can inspect headers, in TCP mode it just shuffles bytes.
The procedure
-
Start with a sane
globalblock. This is the minimum we deploy:global log /dev/log local0 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 ssl-default-bind-ciphers ECDHE+AESGCM:ECDHE+CHACHA20 ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-ticketsThe
stats socketis the runtime API — see HAProxy runtime API. The SSL defaults guarantee a sane TLS posture even if a per-frontendbindline forgets to set them. -
Define
defaultsonce. Two timeouts are the most common production trip-up — set them deliberately:defaults mode http log global option httplog option dontlognull option http-server-close option forwardfor except 127.0.0.0/8 option redispatch retries 3 timeout http-request 10s timeout queue 1m timeout connect 5s timeout client 1m timeout server 1m timeout http-keep-alive 10s timeout check 10s maxconn 30000 -
Add an HTTP frontend. A minimal HTTP-only frontend on port 80 that points at one backend:
frontend fe_http bind *:80 default_backend be_appFor TLS termination, see HAProxy SSL termination. For content switching with ACLs, see HAProxy ACLs and content switching.
-
Add a backend. A backend with two app servers, round-robin balance, and an HTTP health check:
backend be_app balance roundrobin option httpchk GET /healthz HTTP/1.1\r\nHost:\ app.internal http-check expect status 200 server app1 10.0.1.11:8080 check inter 2s fall 3 rise 2 server app2 10.0.1.12:8080 check inter 2s fall 3 rise 2The health check (
option httpchk+http-check expect) is non-optional in production — see HAProxy health checks for the full taxonomy. -
Test, reload, observe. Always in this order:
sudo haproxy -c -f /etc/haproxy/haproxy.cfg sudo systemctl reload haproxy curl -I http://localhost/ sudo journalctl -u haproxy -n 50 --no-pager -
Avoid
listenfor HTTP. Alistenblock combines frontend and backend. It is fine for a single TCP service (an SMTP relay, a Redis sentinel proxy), but for HTTP it removes your ability to add a second backend later without rewriting the section. We uselistenonly for one-off TCP and the stats page.
Operational notes
- A
frontendwithout adefault_backendor anyuse_backendrule will accept connections and return 503 — HAProxy logsNOSRVand the client sees a generic error. Always wire a default. defaultsblocks are positional: adefaultsblock applies only to the sections that come after it in the file. Twodefaultsblocks let you have different timeouts for HTTP and TCP frontends without redefining every directive.option forwardforadds anX-Forwarded-Forheader. If the backend is another reverse proxy, this is what it relies on for the real client IP; do not skip it.- The
chrootin global means file paths in directives likeca-file,errorfile, and Lua scripts are read at startup before chroot, so absolute paths work — but anything HAProxy reads at runtime (rare) must be inside the chroot. retries 3plusoption redispatchis the safety net: if a backend server fails mid-connection, HAProxy will redispatch to another server. Withoutredispatch, the retry happens against the same dead server.
Stack Harbor maintains HAProxy config under Git for every client — one tree per environment, configtest gated through CI, reload via the ops runner, change log shipped to the team. If your haproxy.cfg is a 600-line file that nobody is comfortable touching, we untangle it as part of Managed Operations.