HAProxy can look difficult when you first open haproxy.cfg. It is mostly a
list of small rules. Each rule checks something about the request and then
does an action: send it to a server, redirect it, block it, or change a header.
This article shows rules which are useful on a small Debian server. The same ideas also work on a bigger setup with many servers.
The basic structure
Most HTTP configurations have these sections:
global
# Process-wide settings
defaults
# Settings inherited by frontends and backends
frontend public_https
# Where clients connect
backend my_web_servers
# Where HAProxy sends the requests
The global section is for things like logs, the user running HAProxy, and
the statistics socket. The defaults section contains common timeouts and the
proxy mode. A frontend accepts the connection. A backend contains one or
more real servers.
A listen section can combine a frontend and a backend. It is useful for a
small service, but separate frontends and backends are easier to read when the
configuration grows.
global: settings for the HAProxy process
A small example:
global
log /dev/log local0
stats socket /run/haproxy/admin.sock mode 660 level admin
user haproxy
group haproxy
Why use these rules?
logsends HAProxy messages to syslog. This helps when you need to see why a request failed.stats socketgives local tools a way to ask HAProxy about servers and connections.userandgroupmake HAProxy run with a limited system account.
Do not put passwords or private keys in this section. The configuration file is often readable by administrators and may be copied for backup.
defaults: common rules
defaults
mode http
option httplog
timeout connect 5s
timeout client 50s
timeout server 50s
Why use this?
mode httplets HAProxy read the host, path, method, and headers. This is needed for most routing rules.option httplogmakes the access log useful for HTTP requests.- The timeouts stop a broken client or server from keeping a connection forever.
The timeout values depend on the application. A normal website can use short values. A long download or a slow media application may need a longer server timeout.
frontend: where traffic enters
frontend public_http
bind :80
default_backend web_servers
bind :80 listens on port 80 on all local addresses. You can also bind to one
address, for example bind 192.168.1.10:80, if the service must not listen on
another interface.
Why bind to one address? This is useful when the server has a public and a private network, and the proxy should only accept traffic on one of them.
For HTTPS, the bind can look like this:
frontend public_https
bind :443 ssl crt-list /etc/haproxy/haproxy.crt-list alpn h2,http/1.1
default_backend web_servers
The certificate list tells HAProxy which certificates it can use. The alpn
part allows HTTP/2 and HTTP/1.1 clients.
backend: the application server
backend web_servers
balance roundrobin
server web1 192.168.1.21:8080 check
server web2 192.168.1.22:8080 check
Why use balance roundrobin? Requests are shared between the servers. This is
useful when one server cannot handle all traffic, or when you want to keep the
service available during maintenance.
With only one server, balance is not needed, but keeping the backend is still
useful because the frontend stays clean.
The check at the end of a server line enables health checking. HAProxy can
then stop using a server which does not answer.
Health checks
A simple check tests if the TCP port accepts a connection. For a web service, it is usually better to check a real URL:
backend web_servers
option httpchk GET /health
http-check expect status 200
server web1 127.0.0.1:8080 check
Why use /health? A process can still have its port open while the application
is broken. A health URL can check that the application is really ready.
If the application has no health URL, use / and accept the normal response:
backend web_servers
option httpchk GET /
http-check expect status 200-399
server web1 127.0.0.1:8080 check
Be careful with checks which require authentication. A check which receives a login page may be marked healthy even when the useful part of the application is not working.
ACLs: the main decision tool
An ACL returns true or false. It has no effect until another rule uses it with
if or unless.
frontend public_https
bind :443 ssl crt-list /etc/haproxy/haproxy.crt-list
acl is_blog hdr(host) -i blog.example.com
use_backend blog if is_blog
default_backend web_servers
Why use an ACL? This lets one public IP serve several websites. HAProxy reads
the Host header and sends each request to the correct application.
The -i flag makes the host comparison case-insensitive. You can list more
than one value:
acl is_blog hdr(host) -i blog.example.com www.blog.example.com
This is useful when both the main domain and the www name should reach the
same backend.
Match a URL path
acl is_api path_beg /api/
use_backend api_servers if is_api
Why use this? One domain can have a normal website and a separate API service.
For example, /api/ can go to a Python application and / can go to Apache.
Other useful path matches are:
acl is_login path -i /login
acl is_static path_beg /assets/ /images/
acl is_exact_file path -i /robots.txt /favicon.ico
Use path_beg when a whole path area is needed. Use path when only the exact
path should match.
Match the HTTP method
acl read_method method GET HEAD OPTIONS
http-request deny deny_status 405 if !read_method
Why use this? A read-only service, such as an OPDS library endpoint, may not need POST, PUT, or DELETE. Blocking other methods reduces mistakes and noise.
Do not copy this example for an application which needs POST, such as a login page or a contact form.
Match the client IP
acl local_network src 127.0.0.0/8 192.168.1.0/24
http-request deny deny_status 403 if !local_network
Why use this? It is a simple way to keep an admin page available only from the home network.
Do not rely on this alone for an important service. Check the real network ranges first, and consider client certificates, VPN access, or another strong authentication method.
Match a file of values
acl blocked_ips src -f /etc/haproxy/blocked-ips.txt
http-request deny deny_status 403 if blocked_ips
Why use this? You can update the list without putting a long list of addresses inside the main configuration. It is useful for a small blocklist or an allow list.
The file still has to be protected. A bad blocklist can block real users or allow traffic that should be blocked.
Routing to different backends
The usual order is: define ACLs, use the more specific backends first, and leave a safe default at the end.
frontend public_https
bind :443 ssl crt-list /etc/haproxy/haproxy.crt-list
acl host_main hdr(host) -i example.com
acl host_media hdr(host) -i media.example.com
acl path_api path_beg /api/
use_backend api_servers if host_main path_api
use_backend main_site if host_main
use_backend media_server if host_media
default_backend reject_unknown_host
Why use a safe default? A request with an unknown or wrong Host header does
not accidentally reach an application. This matters when many domains share
one IP address.
The conditions on one line are combined with AND. Therefore the API rule needs
both host_main and path_api to be true.
Redirect HTTP to HTTPS
frontend public_http
bind :80
acl acme_challenge path_beg /.well-known/acme-challenge/
http-request redirect scheme https code 301 if !acme_challenge
default_backend certbot_http
Why use this? Visitors who type http:// are moved to the encrypted URL.
The exception for the ACME path is needed when Certbot uses the HTTP-01 challenge. Without it, certificate renewal may fail. If another service handles the challenge, adapt the backend and path to that setup.
For a domain change, use a fixed redirect instead:
http-request redirect location https://new.example.com%[path]?%[query] code 301
Test redirects with curl -I before making them permanent. A wrong redirect
can create a loop.
Block or reject requests
acl private_area path_beg /private/
http-request deny deny_status 403 if private_area !local_network
Why use this? The backend may contain a private area which should never be public. Rejecting it at HAProxy saves work for the application and gives one central rule.
You can return a simple status without sending the request to a backend:
acl old_path path -i /old-page
http-request redirect location /new-page code 301 if old_path
Use 403 when the resource exists but the client is not allowed. Use 404 if
you do not want to reveal that the path exists. Use 405 when the URL is valid
but the HTTP method is not allowed.
Add or change headers
frontend public_https
option forwardfor
http-request set-header X-Forwarded-Proto https
http-request set-header X-Forwarded-Host %[req.hdr(host)]
Why use these headers? The backend can know the original client IP, host, and scheme. This is important for correct links, secure cookies, application logs, and redirects.
The backend must be configured to trust these headers only from HAProxy. Do not
let a public client choose a trusted X-Forwarded-For value.
You can also add a response header:
http-response set-header X-Content-Type-Options nosniff
Why use this? It tells browsers not to guess a different content type. Add security headers only after checking that they do not break the application.
Change a path before sending it to the backend
backend files_app
http-request replace-path ^/files/?(.*) /\1
server files 127.0.0.1:8080 check
Why use this? The public URL can be /files/photo.jpg, while the application
receives /photo.jpg. This is useful when an application does not know that it
is mounted under a subdirectory.
Path rewrites and cookie rewrites can be difficult to debug. Start with one simple path, test it, and check the HAProxy log and the backend log together.
Rate limiting with a stick table
For a small API, a basic per-IP limit can look like this:
backend api_servers
stick-table type ip size 100k expire 10m store http_req_rate(10s)
http-request track-sc0 src
http-request deny deny_status 429 if { sc_http_req_rate(0) gt 50 }
server api 127.0.0.1:8080 check
Why use this? A client which sends too many requests gets 429 Too Many Requests. This can reduce damage from a noisy script or a simple HTTP flood.
This is not a complete DDoS solution. Many users can share one public IP, and an attacker can use many IP addresses. Login endpoints may need a stricter limit than normal pages.
Simple load balancing choices
backend web_servers
balance roundrobin
Common choices are:
roundrobin: share requests in order. Good for similar servers.leastconn: send the next request to the server with fewer connections. This can help when requests take different amounts of time.first: fill the first server before using the next one. This can save power on servers which can sleep.
Start with roundrobin unless you have a real reason to use another method.
The best choice depends on the application and on whether sessions are stored
locally.
Sticky sessions
Some old applications keep login state only on one server. A cookie can keep a client on the same server:
backend web_servers
cookie SERVERID insert indirect nocache
server web1 192.168.1.21:8080 check cookie web1
server web2 192.168.1.22:8080 check cookie web2
Why use this? The user can stay logged in even when the application does not share sessions between servers.
Shared sessions in the application are usually better. Sticky sessions can make maintenance and failure recovery less smooth.
A small complete example
This example sends one domain to an Apache site, sends /api/ to an API, and
redirects old HTTP requests to HTTPS:
frontend http_in
bind :80
acl acme path_beg /.well-known/acme-challenge/
http-request redirect scheme https code 301 if !acme
default_backend certbot
frontend https_in
bind :443 ssl crt /etc/haproxy/ssl/example.pem alpn h2,http/1.1
option forwardfor
http-request set-header X-Forwarded-Proto https
acl host_example hdr(host) -i example.com
acl path_api path_beg /api/
use_backend api if host_example path_api
use_backend website if host_example
default_backend unknown_host
backend website
option httpchk GET /
http-check expect status 200-399
server apache 127.0.0.1:8080 check
backend api
option httpchk GET /health
http-check expect status 200
server api 127.0.0.1:9000 check
backend certbot
server certbot 127.0.0.1:8080
backend unknown_host
http-request deny deny_status 404
The exact certificate path, ports, and health URLs must match your server.
Test before reload
Always check the configuration first:
sudo haproxy -c -f /etc/haproxy/haproxy.cfg
If the check is successful, reload the service:
sudo systemctl reload haproxy
Then test the important paths:
curl -I https://example.com/
curl -I https://example.com/api/health
curl -I http://example.com/
Check the service and logs if something is wrong:
systemctl status haproxy --no-pager
journalctl -u haproxy -n 100 --no-pager
A good habit is to change one small rule at a time. Keep a working copy of the
configuration, and do not reload a file which was not checked with
haproxy -c.
Final advice
The most useful HAProxy rules are usually simple:
- use
hdr(host)to select a website; - use
path_begto select an application area; - use
checkandhttpchkso broken servers are removed; - use redirects for HTTPS and domain changes;
- use
denyfor admin areas and methods which are not needed; - use headers so the backend knows the original request;
- use stick tables for small, local rate limits.
Start with the smallest rule which solves the problem. Complicated ACLs are harder to review and easier to get wrong.