Skip to content

HAProxy frontend and backend basics

The four-section layout of haproxy.cfg explained — global, defaults, frontend, backend — with a real config you can drop on a box and reload safely.

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 following frontend/backend/listen unless overridden. You can have multiple defaults blocks; each one applies to sections that follow it until another defaults appears.
  • frontend — what HAProxy listens on. Defines bind addresses, the mode (http or tcp), 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

  1. Start with a sane global block. 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-tickets

    The stats socket is the runtime API — see HAProxy runtime API. The SSL defaults guarantee a sane TLS posture even if a per-frontend bind line forgets to set them.

  2. Define defaults once. 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
  3. 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_app

    For TLS termination, see HAProxy SSL termination. For content switching with ACLs, see HAProxy ACLs and content switching.

  4. 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 2

    The health check (option httpchk + http-check expect) is non-optional in production — see HAProxy health checks for the full taxonomy.

  5. 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
  6. Avoid listen for HTTP. A listen block 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 use listen only for one-off TCP and the stats page.

Operational notes

  • A frontend without a default_backend or any use_backend rule will accept connections and return 503 — HAProxy logs NOSRV and the client sees a generic error. Always wire a default.
  • defaults blocks are positional: a defaults block applies only to the sections that come after it in the file. Two defaults blocks let you have different timeouts for HTTP and TCP frontends without redefining every directive.
  • option forwardfor adds an X-Forwarded-For header. If the backend is another reverse proxy, this is what it relies on for the real client IP; do not skip it.
  • The chroot in global means file paths in directives like ca-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 3 plus option redispatch is the safety net: if a backend server fails mid-connection, HAProxy will redispatch to another server. Without redispatch, 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.