HAProxy running in Docker with dynamic configuration is a game-changer, and the most surprising thing is how little state it actually needs to maintain locally.

Let’s see it in action. Imagine you have a few backend services, say api-service running on ports 8001, 8002, and 8003. We want HAProxy to load balance across them, and we want to be able to add or remove these backends without restarting HAProxy.

First, we need a HAProxy configuration file. This isn’t the dynamic part yet, but it sets up the basics and points to where the dynamic configuration will live.

global
    log stdout local0
    daemon

defaults
    mode http
    timeout connect 5s
    timeout client 50s
    timeout server 50s

frontend http_frontend
    bind *:80
    http-request use-service http if { path -i /api }
    default_backend api_backend

backend api_backend
    balance roundrobin
    # This is where the magic happens: load configuration from a file
    # that can be updated without reloading HAProxy.
    server-template api 10 maxconn 32 check
    # We'll populate this with actual server entries dynamically.

Now, the dynamic part. HAProxy can be instructed to watch a directory for configuration snippets. When a file in that directory changes, HAProxy will pick up the changes.

Let’s create a directory for these dynamic configuration snippets: /usr/local/etc/haproxy/conf.d/.

Inside this directory, we’ll create a file for our backend servers. Let’s call it api.cfg.

# /usr/local/etc/haproxy/conf.d/api.cfg
server api-server-1 192.168.1.10:8001 check
server api-server-2 192.168.1.10:8002 check
server api-server-3 192.168.1.10:8003 check

To make HAProxy watch this directory, we modify the api_backend section in our main haproxy.cfg:

backend api_backend
    balance roundrobin
    server-template api 10 maxconn 32 check
    # Add this line to enable dynamic configuration loading
    config-dir /usr/local/etc/haproxy.conf.d

When HAProxy starts, it will read the main config file, then it will scan the config-dir. It finds api.cfg and parses the server entries within it. If we later add a new server, say 192.168.1.10:8004, we just add this line to api.cfg:

server api-server-4 192.168.1.10:8004 check

HAProxy, by default, checks for changes every 5 seconds. So, after a few seconds, it will detect the new file content, parse it, and seamlessly add api-server-4 to the api_backend pool. No reload, no downtime.

Here’s how you’d run this in Docker. We’ll use a Dockerfile to build our HAProxy image.

FROM haproxy:2.8

COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg
COPY conf.d/ /usr/local/etc/haproxy/conf.d/

EXPOSE 80

CMD ["haproxy", "-f", "/usr/local/etc/haproxy/haproxy.cfg"]

And the docker-compose.yml to orchestrate it:

version: '3.8'

services:
  haproxy:
    build: .
    ports:
      - "80:80"
    volumes:
      # Mount a volume for dynamic configuration so we can change it from the host
      - ./conf.d:/usr/local/etc/haproxy/conf.d:ro # Read-only mount is sufficient if changes are external
    networks:
      - app-network

networks:
  app-network:
    driver: bridge

With this setup, you can edit conf.d/api.cfg on your host machine, and HAProxy inside the container will automatically pick up the changes. This is incredibly powerful for managing ephemeral services or scaling backends without service interruption. The server-template directive is key here; it tells HAProxy to expect a certain number of servers and then dynamically populates them from the config-dir. HAProxy doesn’t need to know the exact list of servers at startup if they are managed dynamically.

The real magic is how HAProxy handles configuration updates. It doesn’t re-parse the entire configuration file. Instead, it specifically watches the config-dir for changes to the files it references. When a change is detected, it only processes the modified or new files, merging them into the running configuration. This is why it’s so fast and doesn’t cause downtime. It’s designed to be resilient to external configuration management.

A common pitfall is forgetting to make the config-dir accessible to HAProxy. If you’re mounting it via Docker volumes, ensure the path inside the container matches what’s in your haproxy.cfg. Also, HAProxy typically checks for changes every 5 seconds by default, but this interval can be adjusted.

The next step in mastering HAProxy is understanding how to leverage its built-in statistics socket for real-time monitoring and control of your running configuration.

Want structured learning?

Take the full Haproxy course →