An HAProxy host is still a single traffic entry point when only one node owns the listener address. Pacemaker can move a floating IP address and the HAProxy service between two nodes, so clients keep using one address while the surviving node takes over after a node or service failure.
This active/passive layout uses Corosync for node membership, Pacemaker for resource placement, the IPaddr2 resource agent for the virtual IP, and the systemd resource agent for the haproxy service. Both nodes need the same HAProxy configuration, and the normal systemd service should stay stopped until Pacemaker starts it inside the cluster group.
Use a real floating address in the same subnet as the client-facing interface, keep that address unassigned before the cluster owns it, and configure fencing before trusting the pair for production traffic. The commands below use Ubuntu 26.04 package names, two HAProxy nodes, and a simple HTTP backend pool; replace the node names, addresses, health URI, and backend servers before applying the resource definitions.
$ getent hosts haproxy-a haproxy-b
10.0.0.11 haproxy-a
10.0.0.12 haproxy-b
Use DNS or matching /etc/hosts entries on both nodes. The names used here must match the names passed to pcs.
$ sudo apt update
$ sudo apt install haproxy pacemaker corosync pcs pacemaker-cli-utils resource-agents-base
pacemaker-cli-utils keeps the pcs resource commands usable in minimal Ubuntu installs where recommended packages may not be installed automatically.
$ sudo passwd hacluster
Use a protected administrative password and do not reuse it outside cluster administration. The pcs host auth step sends this credential to each node's pcsd service.
$ sudo systemctl enable --now pcsd
global
log /dev/log local0
defaults
mode http
timeout connect 5s
timeout client 30s
timeout server 30s
frontend fe_http
bind 10.0.0.50:80
default_backend app_pool
backend app_pool
balance roundrobin
option httpchk GET /healthz
http-check expect status 200
server app1 10.0.10.11:8080 check
server app2 10.0.10.12:8080 check
The bind address is the floating IP that Pacemaker will add before starting HAProxy. Use bind :80 only when the node should listen on every local address.
$ sudo haproxy -c -V -f /etc/haproxy/haproxy.cfg Configuration file is valid
$ sudo systemctl disable --now haproxy
Do not leave a boot-enabled standalone haproxy service outside Pacemaker. A node that starts HAProxy before owning the floating IP can fail to bind or can serve traffic outside cluster control.
$ sudo pcs host auth haproxy-a haproxy-b -u hacluster Password: haproxy-a: Authorized haproxy-b: Authorized
$ sudo pcs cluster setup ha-haproxy haproxy-a addr=10.0.0.11 haproxy-b addr=10.0.0.12 --enable --start --wait=120
Current pcs uses knet transport by default and configures two_node: 1 for a two-node Corosync cluster.
$ sudo pcs status Cluster name: ha-haproxy Cluster Summary: * Stack: corosync * 2 nodes configured Node List: * Online: [ haproxy-a haproxy-b ]
$ sudo pcs stonith status * fence_haproxy_a (stonith:fence_ipmilan): Started haproxy-b * fence_haproxy_b (stonith:fence_ipmilan): Started haproxy-a
If this command shows no working fence device, install and configure the fence agent that matches the platform before using the cluster for production traffic. Do not use stonith-enabled=false as a production shortcut.
$ sudo pcs resource defaults update resource-stickiness=100 Warning: Defaults do not apply to resources which override them with their own defined values
$ sudo pcs resource create vip_haproxy ocf:heartbeat:IPaddr2 ip=10.0.0.50 cidr_netmask=24 op monitor interval=10s --group grp_haproxy
The floating IP must be unused before Pacemaker starts managing it. Reusing an address already assigned to a node or another device can cause duplicate-address conflicts on the subnet.
$ sudo pcs resource create svc_haproxy systemd:haproxy op monitor interval=30s --group grp_haproxy
A Pacemaker group keeps the resources colocated and starts them in order. The floating IP starts before the HAProxy service, which matters when HAProxy binds directly to that address.
$ sudo pcs resource config grp_haproxy Group: grp_haproxy Resource: vip_haproxy (class=ocf provider=heartbeat type=IPaddr2) Attributes: cidr_netmask=24 ip=10.0.0.50 Operations: monitor interval=10s Resource: svc_haproxy (class=systemd type=haproxy) Operations: monitor interval=30s
$ sudo pcs status Cluster name: ha-haproxy Node List: * Online: [ haproxy-a haproxy-b ] Full List of Resources: * Resource Group: grp_haproxy: * vip_haproxy (ocf:heartbeat:IPaddr2): Started haproxy-a * svc_haproxy (systemd:haproxy): Started haproxy-a
$ curl http://10.0.0.50/ app1 $ curl http://10.0.0.50/ app2
$ sudo pcs cluster stop haproxy-a haproxy-a: Stopping Cluster (pacemaker)... haproxy-a: Stopping Cluster (corosync)...
This intentionally moves the floating IP and service. Run it only in staging or during a maintenance window where a brief HAProxy failover is acceptable.
$ sudo pcs status Cluster name: ha-haproxy Node List: * Online: [ haproxy-b ] * OFFLINE: [ haproxy-a ] Full List of Resources: * Resource Group: grp_haproxy: * vip_haproxy (ocf:heartbeat:IPaddr2): Started haproxy-b * svc_haproxy (systemd:haproxy): Started haproxy-b
$ curl http://10.0.0.50/ app2
$ sudo pcs cluster start haproxy-a
haproxy-a: Starting Cluster...
The group can stay on haproxy-b after haproxy-a returns because resource stickiness now favors the current healthy node.