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.

Steps to create a highly available HAProxy cluster with Pacemaker:

  1. Confirm that both cluster node names resolve from each HAProxy node.
    $ 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.

  2. Refresh the package metadata on both nodes.
    $ sudo apt update
  3. Install HAProxy, Pacemaker, Corosync, pcs, Pacemaker CLI helpers, and the supported base resource agents on both nodes.
    $ 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.

  4. Set the hacluster password on both nodes.
    $ 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.

  5. Start and enable pcsd on both nodes.
    $ sudo systemctl enable --now pcsd
  6. Create the same HAProxy configuration on both nodes.
    /etc/haproxy/haproxy.cfg
    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.

  7. Validate the HAProxy configuration on both nodes.
    $ sudo haproxy -c -V -f /etc/haproxy/haproxy.cfg
    Configuration file is valid
  8. Stop and disable the standalone HAProxy service on both nodes so Pacemaker becomes the only service manager for it.
    $ 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.

  9. Authenticate the two nodes from one cluster node.
    $ sudo pcs host auth haproxy-a haproxy-b -u hacluster
    Password:
    haproxy-a: Authorized
    haproxy-b: Authorized
  10. Create and start the two-node cluster from one cluster node.
    $ 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.

  11. Confirm both nodes are online.
    $ sudo pcs status
    Cluster name: ha-haproxy
    Cluster Summary:
      * Stack: corosync
      * 2 nodes configured
     
    Node List:
      * Online: [ haproxy-a haproxy-b ]
  12. Confirm that production fencing is configured before serving traffic through the floating IP.
    $ 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.

  13. Set resource stickiness so a recovered node does not pull a healthy HAProxy group back unnecessarily.
    $ sudo pcs resource defaults update resource-stickiness=100
    Warning: Defaults do not apply to resources which override them with their own defined values
  14. Create the floating IP resource and place it in a new HAProxy group.
    $ 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.

  15. Add the HAProxy systemd service resource to the same group.
    $ 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.

  16. Check the Pacemaker resource configuration.
    $ 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
  17. Confirm the group is started on one node.
    $ 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
  18. Send requests to the floating IP to verify traffic reaches the backend pool through HAProxy.
    $ curl http://10.0.0.50/
    app1
    $ curl http://10.0.0.50/
    app2
  19. Stop the cluster stack on the node currently running the HAProxy group during an approved test window.
    $ 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.

  20. Confirm the HAProxy group moved to the surviving node.
    $ 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
  21. Request the floating IP again after failover.
    $ curl http://10.0.0.50/
    app2
  22. Start the stopped cluster node again.
    $ 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.