Skip to main content

HAProxy Docker Compose: Config, HTTPS and Reload (2026)

To run HAProxy with Docker Compose, use the haproxy:3.4 image, mount your haproxy.cfg at /usr/local/etc/haproxy/haproxy.cfg, and publish ports 80 and 443. A frontend receives traffic, a backend lists your app containers by service name. HAProxy does not get Let's Encrypt certificates on its own in a stable way, so a Certbot container issues them and writes one combined PEM file per domain for HAProxy. Apply changes without downtime with docker compose kill -s USR2 haproxy.

Every file on this page was tested with HAProxy 3.4 (the current LTS), Certbot 5.8 and Docker 29.

Comparing proxies first? See HAProxy alternatives.

Before you start​

  • A Linux server with Docker and Compose. See Install Docker Compose.
  • A domain with an A record that points to the server. This guide uses example.com.
  • Ports 80 and 443 open in the firewall. Let's Encrypt connects to port 80 to check that you own the domain.

The Compose file​

Create a folder such as /srv/proxy and put this compose.yaml in it:

services:
haproxy:
image: haproxy:3.4
ports:
- "80:80"
- "443:443"
volumes:
- ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
- certs:/usr/local/etc/haproxy/certs:ro
restart: unless-stopped

app:
image: traefik/whoami
restart: unless-stopped

certbot:
image: certbot/certbot
command: renew
volumes:
- letsencrypt:/etc/letsencrypt
- certs:/certs
profiles: ["certbot"]

volumes:
letsencrypt:
certs:
  • app is your application. Replace traefik/whoami with your image.
  • certbot has a profile, so docker compose up -d does not start it. You run it only to get or renew certificates.
  • The certs volume is shared. Certbot writes PEM files into it, HAProxy reads them.

The HAProxy config​

haproxy.cfg:

global
log stdout format raw local0

defaults
mode http
log global
option httplog
option forwardfor
timeout connect 5s
timeout client 30s
timeout server 30s

# Docker's internal DNS. Containers get a new IP when they are recreated.
resolvers docker
nameserver dns 127.0.0.11:53
hold valid 10s

frontend web
bind :80
bind :443 ssl crt /usr/local/etc/haproxy/certs/
acl acme path_beg /.well-known/acme-challenge/
http-request redirect scheme https code 301 unless { ssl_fc } || acme
use_backend certbot if acme
default_backend app

backend app
server app1 app:80 check resolvers docker init-addr libc,none

backend certbot
server certbot certbot:80 resolvers docker init-addr none
PartWhat it does
log stdoutLogs go to docker compose logs haproxy.
mode httpHAProxy reads HTTP, so it can route by host and path. Use mode tcp for databases or other non-HTTP traffic.
option forwardforAdds X-Forwarded-For with the client IP.
timeout ...HAProxy warns at start if these are missing.
resolvers dockerHAProxy asks Docker's DNS for app every 10 seconds and follows new IPs.
bind :443 ssl crt .../certs/Loads every PEM file in the folder. HAProxy picks the right one per domain (SNI).
acl acme + use_backend certbotLet's Encrypt challenges go to the Certbot container. Everything else on port 80 is redirected to HTTPS.
init-addr libc,noneHAProxy starts even when app is not running yet.
checkHealth checks. A failing server gets no traffic.

Get the first certificate​

HAProxy will not start with an empty certificate folder. Get the first certificate before you start it. Certbot runs its own small web server on port 80 for this:

HOOK='cat $RENEWED_LINEAGE/fullchain.pem $RENEWED_LINEAGE/privkey.pem > /certs/$(basename $RENEWED_LINEAGE).pem && chown 99:99 /certs/*.pem && chmod 600 /certs/*.pem'

docker compose run --rm -p 80:80 certbot certonly --standalone \
-d example.com --email you@example.com --agree-tos --no-eff-email \
--deploy-hook "$HOOK"

Then start HAProxy:

docker compose up -d

The deploy hook is the important part. HAProxy needs the certificate and the key in one file. It also runs as user haproxy (UID 99) and cannot read Certbot's own files, which are readable by root only. The hook writes /certs/example.com.pem, owned by UID 99. Certbot saves the hook and runs it again after every renewal.

Renew certificates​

Let's Encrypt certificates last 90 days. Add a cron job on the host that runs Certbot and reloads HAProxy:

# crontab -e
0 3 * * * cd /srv/proxy && docker compose --profile certbot up certbot && docker compose kill -s USR2 haproxy

renew only renews certificates that expire within 30 days. On all other days it does nothing. During renewal, HAProxy keeps running and forwards the Let's Encrypt challenge to the Certbot container. USR2 makes HAProxy load the new certificate with no dropped connections.

We tested this with a forced renewal: after USR2, HAProxy served the new certificate.

Add another site​

Add a backend and a routing rule by host name:

frontend web
# ... as above, plus, before default_backend:
use_backend api if { req.hdr(host),host_only -i api.example.com }
default_backend app

backend api
server api1 api:8000 check resolvers docker init-addr libc,none

Get a certificate for the new domain while HAProxy is running. --use-aliases gives the Certbot container its service name on the network, so HAProxy can forward the challenge to it:

docker compose --profile certbot run --rm --use-aliases certbot certonly --standalone \
-d api.example.com --email you@example.com --agree-tos --no-eff-email \
--deploy-hook "$HOOK"
docker compose kill -s USR2 haproxy

HAProxy now serves api.example.com.pem for the new domain. Apps in a different Compose project need a shared Docker network. See Docker Compose networks.

Check and reload the config​

docker compose exec haproxy haproxy -c -V -f /usr/local/etc/haproxy/haproxy.cfg
docker compose kill -s USR2 haproxy

haproxy -c -V checks the file and prints Configuration file is valid. USR2 reloads it. Old connections finish on the old process; new ones use the new config.

The compose file mounts haproxy.cfg as a single file. Some editors save a file by replacing it, and the container then keeps the old version. If a reload shows no change, run docker compose up -d --force-recreate haproxy.

Load balancing​

List several servers in one backend:

backend app
balance roundrobin
server app1 app1:80 check resolvers docker init-addr libc,none
server app2 app2:80 check resolvers docker init-addr libc,none

balance leastconn sends each request to the server with the fewest open connections. It works better for long requests and WebSockets.

The built-in ACME client​

HAProxy 3.4 has its own ACME client, but it is still experimental. It needs expose-experimental-directives, and it keeps new certificates only in memory: after a restart they are gone unless you save them through the runtime API. For a server you depend on, Certbot is the safer choice today.

Troubleshooting​

503 Service Unavailable​

No healthy server in the backend. Run docker compose logs haproxy. A line like Server app/app1 is DOWN means the health check fails: the port is wrong, the app listens only on 127.0.0.1 inside its container, or the app is not on the same Docker network.

HAProxy does not start: no SSL certificate specified for bind ':443'​

The certs folder is empty. If the log says unable to stat SSL certificate instead, the path in crt is wrong. Get the first certificate before you start HAProxy (see above).

cannot open the file or Permission denied for a certificate​

HAProxy runs as UID 99 and cannot read root-only files. Point HAProxy at the combined files in the certs volume, not at /etc/letsencrypt/live/, and keep the chown 99:99 in the hook.

Renewal fails with Connection refused or a timeout​

HAProxy cannot reach the Certbot container. Use docker compose --profile certbot up certbot for renewal, or run --use-aliases. A plain docker compose run container has no service name on the network.

ERR_TOO_MANY_REDIRECTS​

Cloudflare is in Flexible SSL mode and talks to HAProxy over HTTP. Set Cloudflare's SSL mode to Full (strict).

Install HAProxy without Docker (Ubuntu / Debian)​

sudo apt update
sudo apt install haproxy

The config is in /etc/haproxy/haproxy.cfg. Use server app1 127.0.0.1:3000 check for an app on the same server and drop the resolvers lines. Check with sudo haproxy -c -f /etc/haproxy/haproxy.cfg and apply with sudo systemctl reload haproxy. Ubuntu's own package can be several versions behind. For the current LTS, use the PPA or Debian backports listed on haproxy.debian.net.

When to use HAProxy​

  • You need load balancing with health checks, sticky sessions or detailed metrics.
  • You proxy TCP traffic too, such as databases or mail.
  • You need very high throughput with low CPU use.

When not to use it:

Frequently asked questions

How do I run HAProxy with Docker Compose?

Use the haproxy:3.4 image, mount your config at /usr/local/etc/haproxy/haproxy.cfg, and publish ports 80 and 443. In the config, a frontend binds the ports and a backend lists your app containers by Compose service name, for example server app1 app:80 check.

How do I use Let's Encrypt with HAProxy in Docker?

Run Certbot in its own container. Issue the first certificate with certbot certonly --standalone before HAProxy starts. A deploy hook concatenates fullchain.pem and privkey.pem into one file per domain in a volume HAProxy reads. Renew with a daily cron job that runs certbot renew and then sends USR2 to HAProxy.

How do I reload HAProxy in Docker without downtime?

Check the config with docker compose exec haproxy haproxy -c -V -f /usr/local/etc/haproxy/haproxy.cfg, then run docker compose kill -s USR2 haproxy. The official image runs HAProxy in master-worker mode, and USR2 starts new workers while old connections finish.

Why does HAProxy return 503 Service Unavailable?

No server in the backend is healthy. The health check fails because the port is wrong, the app listens on 127.0.0.1 inside its container, or the app is on another Docker network. The HAProxy log shows which server is DOWN and why.

Does HAProxy support Let's Encrypt natively?

HAProxy 3.4 has a built-in ACME client, but it is experimental, needs expose-experimental-directives, and keeps certificates only in memory. Most setups still use Certbot or acme.sh next to HAProxy.

HAProxy or Nginx?

HAProxy for load balancing, health checks, TCP proxying and detailed traffic metrics. Nginx when you also need a web server for static files, caching or rewrites. Both handle a simple reverse proxy well.



Deploy without managing HAProxy​

Hostim.dev gives every app HTTPS, a domain, logs and metrics. No proxy config, no Certbot cron job.

👉 Deploy an app with automatic HTTPS