Configure HAProxy with Consul for dynamic service discovery and automatic backend updates

Advanced 75 min Aug 23, 2026
Ubuntu 24.04 Debian 12 AlmaLinux 9 Rocky Linux 9

Build a self-updating load balancer by integrating HAProxy with Consul service discovery, consul-template, and health checks so backends register and deregister automatically without manual reloads.

Prerequisites

  • Two or more Linux servers with root or sudo access
  • Basic familiarity with HAProxy configuration syntax
  • Network connectivity between hosts on Consul's default ports
  • An application with a health check endpoint to register as a service

What this solves

Static HAProxy backend lists break down once services scale up, restart, or move between hosts. This tutorial wires HAProxy to Consul's service catalog using consul-template, so backend servers are added, removed, and health-checked automatically as your topology changes.

By the end, you will have Consul agents registering services, consul-template regenerating HAProxy configuration on catalog changes, reload automation without dropping connections, and ACL plus TLS hardening for the whole pipeline.

Step-by-step configuration

Install HAProxy and the Consul agent

Both packages ship in official repositories on Ubuntu, Debian, AlmaLinux and Rocky. Install HAProxy first, then add HashiCorp's repository for Consul.

sudo apt update
sudo apt install -y haproxy curl gnupg lsb-release
curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update
sudo apt install -y consul consul-template
sudo dnf install -y haproxy curl dnf-plugins-core
sudo dnf config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo
sudo dnf install -y consul consul-template

Create a dedicated Consul data directory and user

Consul should never run as root. Create a system user with a locked-down home directory for its data.

sudo useradd --system --home /var/lib/consul --shell /usr/sbin/nologin consul
sudo mkdir -p /var/lib/consul /etc/consul.d
sudo chown -R consul:consul /var/lib/consul
sudo chmod 750 /var/lib/consul
Note: 750 lets the consul user read and write freely while blocking access from other unprivileged accounts on the host.

Configure the Consul agent

This example runs a single-node Consul server for the tutorial. In production you would run 3 or 5 server nodes for quorum, similar to the multi-datacenter setup in Consul multi-datacenter replication with WAN federation.

datacenter = "dc1"
data_dir = "/var/lib/consul"
server = true
bootstrap_expect = 1
bind_addr = "203.0.113.10"
client_addr = "127.0.0.1"
ui_config {
  enabled = true
}
connect {
  enabled = true
}
sudo chown -R consul:consul /etc/consul.d
sudo chmod 640 /etc/consul.d/consul.hcl

Create a systemd unit for Consul

Run Consul as a managed service so it restarts automatically and logs to journald.

[Unit]
Description=Consul service discovery agent
After=network-online.target
Wants=network-online.target

[Service]
User=consul
Group=consul
ExecStart=/usr/bin/consul agent -config-dir=/etc/consul.d
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now consul
sudo systemctl status consul

Register application services and health checks

Define each backend as a Consul service with an HTTP health check. Consul only advertises healthy instances to HAProxy.

{
  "service": {
    "name": "web-app",
    "port": 8080,
    "address": "203.0.113.21",
    "tags": ["http"],
    "check": {
      "http": "http://203.0.113.21:8080/healthz",
      "interval": "5s",
      "timeout": "2s",
      "deregister_critical_service_after": "1m"
    }
  }
}
sudo chown consul:consul /etc/consul.d/web-app.json
sudo consul reload

Repeat this file on each backend host with its own address, or push the definition remotely with the Consul HTTP API in a later step. Verify registration:

consul catalog services
consul health checks web-app

Write the consul-template source template

consul-template watches the Consul catalog and renders a new HAProxy config whenever the set of healthy instances changes.

global
    log /dev/log local0
    maxconn 4096

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

frontend web_front
    bind *:80
    default_backend web_back

backend web_back
    balance roundrobin
    option httpchk GET /healthz
{{range service "web-app"}}    server {{.Node}}-{{.Port}} {{.Address}}:{{.Port}} check
{{end}}

Configure consul-template to write HAProxy config and reload

The wait and dedup blocks smooth out rapid catalog churn so HAProxy is not reloaded on every single check flap.

consul {
  address = "127.0.0.1:8500"
}

template {
  source      = "/etc/consul-template/haproxy.ctmpl"
  destination = "/etc/haproxy/haproxy.cfg"
  command     = "sudo systemctl reload haproxy"
  command_timeout = "30s"
}

wait {
  min = "3s"
  max = "15s"
}
[Unit]
Description=Consul Template for HAProxy backends
After=consul.service
Requires=consul.service

[Service]
User=root
ExecStart=/usr/bin/consul-template -config=/etc/consul-template/config.hcl
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
Warning: consul-template needs permission to run systemctl reload haproxy. Grant this with a scoped sudoers rule instead of running the whole service as root without limits.
consul-template ALL=(root) NOPASSWD: /usr/bin/systemctl reload haproxy
sudo systemctl daemon-reload
sudo systemctl enable --now consul-template
sudo systemctl status consul-template

Validate and enable HAProxy

Always validate the generated configuration before trusting automatic reloads in production.

sudo haproxy -c -f /etc/haproxy/haproxy.cfg
sudo systemctl enable --now haproxy
sudo systemctl status haproxy

For graceful reloads with zero dropped connections, review the reload strategy covered in HAProxy performance tuning with connection pooling, since consul-template simply calls systemctl reload under the hood.

Set up Consul DNS forwarding

Consul's built-in DNS server lets applications resolve service names like web-app.service.consul directly, which is useful for services that talk to each other without going through HAProxy.

[Resolve]
DNS=127.0.0.1:8600
Domains=~consul
sudo mkdir -p /etc/systemd/resolved.conf.d
sudo systemctl restart systemd-resolved
dig @127.0.0.1 -p 8600 web-app.service.consul

Use the HTTP API for programmatic registration from deployment pipelines instead of hand-editing JSON files on every host:

curl --request PUT --data '{
  "Name": "web-app",
  "Port": 8080,
  "Address": "203.0.113.22",
  "Check": {
    "HTTP": "http://203.0.113.22:8080/healthz",
    "Interval": "5s"
  }
}' http://127.0.0.1:8500/v1/agent/service/register

Test failover and dynamic registration

Simulate a backend failure and confirm HAProxy removes it without manual intervention.

curl -s http://127.0.0.1:8500/v1/health/service/web-app?passing | jq '.[].Service.Address'
sudo systemctl stop web-app
sleep 15
cat /etc/haproxy/haproxy.cfg | grep server
sudo systemctl start web-app

Register a brand new instance on a fresh host and confirm it appears in the backend within a few seconds, without touching HAProxy configuration by hand.

Secure the cluster with ACLs

Default Consul installs allow any client to read and write the catalog. Enable ACLs with a default-deny policy and issue scoped tokens.

acl {
  enabled = true
  default_policy = "deny"
  enable_token_persistence = true
}
sudo systemctl restart consul
consul acl bootstrap

Save the bootstrap token output securely, then create a read-only token for consul-template.

service_prefix "" {
  policy = "read"
}
node_prefix "" {
  policy = "read"
}
consul acl policy create -name "template-read" -rules @/etc/consul.d/template-policy.hcl
consul acl token create -description "consul-template read token" -policy-name "template-read"

Add the resulting token to the consul-template configuration:

consul {
  address = "127.0.0.1:8500"
  token   = "YOUR-GENERATED-TOKEN-HERE"
}
sudo chmod 600 /etc/consul-template/config.hcl
sudo systemctl restart consul-template

For a deeper walkthrough of policy design, see advanced Consul ACL policies for production security hardening.

Enable TLS for agent communication

Encrypt Consul's RPC and gossip traffic so catalog data and tokens are not exposed on the network.

consul tls ca create
consul tls cert create -server -dc dc1
tls {
  defaults {
    ca_file   = "/etc/consul.d/consul-agent-ca.pem"
    cert_file = "/etc/consul.d/dc1-server-consul-0.pem"
    key_file  = "/etc/consul.d/dc1-server-consul-0-key.pem"
    verify_incoming = true
    verify_outgoing = true
  }
}
sudo chown consul:consul /etc/consul.d/*.pem
sudo chmod 600 /etc/consul.d/*-key.pem
sudo chmod 644 /etc/consul.d/*.pem
sudo systemctl restart consul
Never use chmod 777. Private key files must stay readable only by the consul user. Broad permissions let any local account on the host read your TLS keys and impersonate agents.

For HAProxy's public-facing TLS, follow HAProxy SSL termination with Let's Encrypt certificates to terminate client traffic securely before it reaches the dynamic backend pool.

Verify your setup

sudo systemctl status consul consul-template haproxy
consul members
consul catalog services
curl -I http://127.0.0.1/
sudo haproxy -c -f /etc/haproxy/haproxy.cfg

Common issues

SymptomCauseFix
HAProxy config never updatesconsul-template cannot reach Consul or lacks a valid ACL tokenCheck journalctl -u consul-template -f and verify the token has service read permissions
HAProxy reload fails silentlyconsul-template user lacks sudo rights for systemctlConfirm the sudoers rule in /etc/sudoers.d/consul-template matches the exact command
Backend flaps constantlyHealth check interval too aggressive or app slow to respondIncrease interval and timeout values in the service check definition
Consul agent won't join clusterFirewall blocking gossip ports 8301/8302Open the specific TCP/UDP ports with your firewall tool instead of disabling it
consul-template permission denied writing configDestination file owned by a different userchown root:root /etc/haproxy/haproxy.cfg and rerun as the correct service user
ACL bootstrap fails with "already bootstrapped"Bootstrap already ran on this clusterRecover using an existing management token instead of re-bootstrapping

Next steps

Running this in production?

Want this handled for you? Running this at scale adds a second layer of work: ACL and token rotation, TLS certificate renewal across every agent, reload storms during deploys, and 24/7 on-call for catalog failures. See how we run infrastructure like this for European teams.

Automated install script

Run this to automate the entire setup

Sie möchten das nicht selbst verwalten?

Wir betreiben Infrastruktur für Unternehmen, die auf Verfügbarkeit angewiesen sind. Vollständig verwaltet, mit einem festen Ansprechpartner, der Ihre Umgebung kennt.

Sie erhalten einen festen Ansprechpartner, der Ihr Setup kennt

Rotterdam 14:01 · erreichbar per Nachricht, kein Ticketformular