← Back to list

How to Build a Highly Available MySQL Setup with HAProxy and Keepalived

A step-by-step tutorial for load balancing two MySQL clusters through a single floating virtual IP, with automatic failover between two…

DevOps voice in Beyond Localhost · 2026-07-13 04:42 · 55 claps · 8.5 min read paywalled
#haproxy #devops-practice #load-balancing #mysql-dba #linux-tutorial
Open on Medium ↗
Wiki topics: ☁️ · DevOps & Cloud 🔓 · Open Source 💑 · Relationships

How to Build a Highly Available MySQL Setup with HAProxy and Keepalived

A step-by-step tutorial for load balancing two MySQL clusters through a single floating virtual IP, with automatic failover between two HAProxy nodes

▶️ **YouTube , [📸 Instagram](https://www.instagram.com/devops_voice) , [💼 LinkedIn](https://www.linkedin.com/in/tushar-jadhav29/) , [✍️ Medium](https://medium.com/@tushar.jadhav29)**

**Non-Member= Click HERE!**

Introduction

A single load balancer is a single point of failure. If it goes down, it doesn’t matter how resilient your MySQL clusters are — nothing can reach them.

This guide walks through a production-style setup that removes that weak point:

  • Two HAProxy nodes compiled from source and installed under a custom path
  • Keepalived running VRRP between them, sharing one floating virtual IP (VIP)
  • Two independent MySQL clusters — one on port 3306, one on port 3308 — each load balanced through its own HAProxy listener
  • systemd services for both HAProxy and Keepalived, so everything survives a reboot
  • A working failover test so you know it actually works before you trust it in production

By the end, you’ll have a setup where either HAProxy node can go down and traffic keeps flowing through the VIP without a single manual step.

HAProxy High Availability Architecture

Architecture overview

Here’s the full picture before we touch a terminal:

  • Clients connect to one address only: the virtual IP, 10.x.x.10
  • Keepalived decides which HAProxy node currently owns that IP, using the VRRP protocol
  • The active node (MASTER) holds the VIP and forwards connections; the standby node (BACKUP) sits idle, watching for a heartbeat
  • Each HAProxy instance runs two independent listeners:
  • 3306 → MySQL cluster 1 (db1, db2)
  • 3308 → MySQL cluster 2 (db3, db4)
  • Inside each listener, HAProxy uses mysql-check to confirm a backend is actually alive before sending it traffic, and balance first to concentrate connections on the primary node while keeping the second as a hot spare

If node A’s HAProxy process dies, Keepalived detects it within a couple of seconds and moves the VIP to node B automatically — no DNS changes, no application reconfiguration, no manual intervention.

Prerequisites

Before you start, confirm two things:

  1. Network interface name. Run ip a on both nodes. This guide uses eth0, but cloud VMs (Azure, AWS, GCP) often use ens5, eth0, or similar — adjust the interface line in keepalived.conf to match.
  2. What mysql-check does and doesn't do. HAProxy's built-in MySQL check confirms the backend accepts a connection — it does not check the read_only flag. If you need HAProxy to route writes only to the writable node, you'll want an agent-based check instead of the built-in one (see the FAQ below).

You’ll also need:

  • Two Linux servers to run HAProxy + Keepalived (referred to as Node A and Node B)
  • Two MySQL clusters already running, reachable at their internal IPs
  • Root or sudo access on both HAProxy nodes
  • A free private IP on the same subnet to use as the VIP

Step 1 — Prepare the directory structure

Everything in this guide installs under /data/softwares instead of the usual system paths. This keeps HAProxy and Keepalived self-contained, easy to back up, and easy to remove cleanly.

sudo mkdir -p /data/softwares/src
sudo mkdir -p /data/softwares/haproxy/{sbin,etc,run,lib}
sudo mkdir -p /data/softwares/keepalived
/data/softwares/
├── haproxy/          ← HAProxy install
│   ├── sbin/haproxy
│   ├── etc/haproxy.cfg
│   └── run/
├── keepalived/       ← Keepalived install
│   └── etc/keepalived.conf
└── src/              ← Source tarballs

Installation Flow

Step 2 — Install HAProxy from source

Install build dependencies

sudo yum groupinstall "Development Tools" -y
sudo yum install openssl-devel pcre2-devel systemd-devel zlib-devel lua-devel wget -y

Download HAProxy 3.4 (LTS)

cd /data/softwares/src
sudo wget https://www.haproxy.org/download/3.4/src/haproxy-3.4.0.tar.gz
sudo tar -xzvf haproxy-3.4.0.tar.gz
cd haproxy-3.4.0

Compile with a custom prefix

sudo make TARGET=linux-glibc \
  USE_OPENSSL=1 \
  USE_PCRE2=1 \
  USE_ZLIB=1 \
  USE_LUA=1 \
  USE_SYSTEMD=1 \
  PREFIX=/data/softwares/haproxy

sudo make install PREFIX=/data/softwares/haproxy

This puts the binary at /data/softwares/haproxy/sbin/haproxy.

Verify the build

/data/softwares/haproxy/sbin/haproxy -v

Create a dedicated system user

Running HAProxy as its own unprivileged user limits the blast radius if it’s ever compromised.

sudo useradd -r -s /sbin/nologin haproxy

sudo chown -R haproxy:haproxy /data/softwares/haproxy/run
sudo chown -R haproxy:haproxy /data/softwares/haproxy/lib

Create the systemd service

sudo vi /etc/systemd/system/haproxy.service
## ini

[Unit]
Description=HAProxy Load Balancer
After=network-online.target
Wants=network-online.target

[Service]
Environment="CONFIG=/data/softwares/haproxy/etc/haproxy.cfg" "PIDFILE=/data/softwares/haproxy/run/haproxy.pid"
ExecStartPre=/data/softwares/haproxy/sbin/haproxy -f $CONFIG -c -q
ExecStart=/data/softwares/haproxy/sbin/haproxy -Ws -f $CONFIG -p $PIDFILE
ExecReload=/data/softwares/haproxy/sbin/haproxy -f $CONFIG -c -q
ExecReload=/bin/kill -USR2 $MAINPID
Restart=always
Type=notify

[Install]
WantedBy=multi-user.target

Note the ExecStartPre line — HAProxy validates the config before attempting to start, so a typo in haproxy.cfg fails loudly at startup instead of silently breaking traffic.

Repeat Step 2 on both HAProxy nodes.

Step 3 — Create the MySQL health-check user

HAProxy needs a MySQL user to run its health checks against. On both nodes of each MySQL cluster, run:

## sql 
CREATE USER 'haproxy_check'@'%' IDENTIFIED BY '';
FLUSH PRIVILEGES;

This account needs no privileges beyond connecting — HAProxy’s mysql-check only opens and closes a connection to confirm the server responds.

Step 4 — Configure HAProxy

sudo vi /data/softwares/haproxy/etc/haproxy.cfg
## haproxy

global
    log /dev/log local0
    maxconn 4096
    user haproxy
    group haproxy
    daemon
    stats socket /data/softwares/haproxy/run/admin.sock mode 660 level admin

defaults
    log     global
    mode    tcp
    option  tcplog
    retries 3
    timeout connect 5s
    timeout client  60m
    timeout server  60m

#---------------------------------------
# Stats page
#---------------------------------------
listen stats
    bind *:8404
    mode http
    stats enable
    stats uri /stats
    stats refresh 5s
    stats admin if TRUE
#---------------------------------------
# MySQL Cluster 1 (Port 3306)
#---------------------------------------
listen mysql-cluster1
    bind *:3306
    mode tcp
    option mysql-check user haproxy_check
    balance first
    server db1 10.0.1.11:3306 check port 3306 inter 2s rise 2 fall 2
    server db2 10.0.1.12:3306 check port 3306 inter 2s rise 2 fall 2 backup
#---------------------------------------
# MySQL Cluster 2 (Port 3308)
#---------------------------------------
listen mysql-cluster2
    bind *:3308
    mode tcp
    option mysql-check user haproxy_check
    balance first
    server db3 10.0.1.13:3308 check port 3308 inter 2s rise 2 fall 2
    server db4 10.0.1.14:3308 check port 3308 inter 2s rise 2 fall 2 backup

A few details worth understanding, not just copying:

  • **balance first** sends all traffic to the first available server and only spills over to the next once it's saturated or down — ideal when one node should stay primary and the other should mostly sit as a hot spare.
  • **backup** on db2 and db4 marks them as fallback servers: they only receive traffic when the primary in their listener fails its health check.
  • **inter 2s rise 2 fall 2** means HAProxy checks each backend every 2 seconds and needs 2 consecutive successes or failures before flipping its state — this avoids flapping on a single slow response.

Validate and start

/data/softwares/haproxy/sbin/haproxy -f /data/softwares/haproxy/etc/haproxy.cfg -c

sudo systemctl daemon-reload
sudo systemctl enable haproxy
sudo systemctl start haproxy
sudo systemctl status haproxy

Repeat this configuration on both nodes — they should be identical.

Step 5 — Install Keepalived from source

Install build dependencies

sudo yum install openssl-devel libnl3-devel ipset-devel iptables-devel gcc make wget -y

Download and compile with a custom prefix

cd /data/softwares/src
sudo wget https://www.keepalived.org/software/keepalived-2.3.1.tar.gz
sudo tar -xzvf keepalived-2.3.1.tar.gz
cd keepalived-2.3.1

sudo ./configure \
  --prefix=/data/softwares/keepalived \
  --sysconfdir=/data/keepalived

sudo make
sudo make install

Verify

/data/softwares/keepalived/sbin/keepalived -v

Create the systemd service

sudo vi /etc/systemd/system/keepalived.service
## ini

[Unit]
Description=Keepalived VRRP
After=network-online.target
Wants=network-online.target

[Service]
Type=forking
PIDFile=/run/keepalived.pid
ExecStart=/data/softwares/keepalived/sbin/keepalived -f /data/softwares/keepalived/etc/keepalived/keepalived.conf -p /run/keepalived.pid
ExecReload=/bin/kill -HUP $MAINPID
Restart=always

[Install]
WantedBy=multi-user.target
sudo mkdir -p /data/softwares/keepalived/etc/keepalived

Repeat on both nodes.

Step 6 — Configure Keepalived for VRRP failover

This is the piece that actually creates the virtual IP and moves it between nodes. The two config files are almost identical — only state and priority differ.

Show Image

Node A (MASTER)

sudo vi /data/softwares/keepalived/etc/keepalived/keepalived.conf

global_defs {
    router_id HAPROXY_A   # Add entry in hosts file for dsn resolution #
    enable_script_security
    script_user root
}

vrrp_script chk_haproxy {
    script "/usr/bin/killall -0 haproxy"
    interval 2
    weight   -20
    fall     2
    rise     2
}
vrrp_instance VI_1 {
    state MASTER
    interface eth0
    virtual_router_id 55
    priority 110
    advert_int 1
    authentication {
        auth_type PASS
        auth_pass haPass123
    }
    virtual_ipaddress {
        10.0.1.10/24
    }
    track_script {
        chk_haproxy
    }
}

Node B (BACKUP)

sudo vi /data/softwares/keepalived/etc/keepalived/keepalived.conf

global_defs {
    router_id HAPROXY_B # Add entry in hosts file for dsn resolution #
    enable_script_security
    script_user root
}

vrrp_script chk_haproxy {
    script "/usr/bin/killall -0 haproxy"
    interval 2
    weight   -20
    fall     2
    rise     2
}
vrrp_instance VI_1 {
    state BACKUP
    interface eth0
    virtual_router_id 55
    priority 100
    advert_int 1
    authentication {
        auth_type PASS
        auth_pass haPass123
    }
    virtual_ipaddress {
        10.0.1.10/24
    }
    track_script {
        chk_haproxy
    }
}

What each piece is doing:

  • **priority 110 vs 100** — the higher number wins the VIP when both nodes are healthy. Node A is preferred by default.
  • **vrrp_script chk_haproxy** — Keepalived runs killall -0 haproxy every 2 seconds. If the process is gone, it subtracts weight -20 from that node's priority, which drops it below the other node and triggers a failover.
  • **virtual_router_id** must match on both nodes, and must be unique on the subnet if you're running more than one VRRP group.
  • **auth_pass** is a plaintext shared secret between the two nodes — treat your real value like a credential, not just a config placeholder.

Start Keepalived on both nodes

sudo systemctl daemon-reload
sudo systemctl enable keepalived
sudo systemctl start keepalived
sudo systemctl status keepalived

Step 7 — Test and verify everything

Don’t skip this step. A failover setup you haven’t tested is just a hope.

Confirm which node currently holds the VIP

ip addr show eth0 | grep 10.0.1.10

Run this on both nodes — only the current MASTER should show the address.

Check backend health from HAProxy’s side

echo "show stat" | sudo socat stdio /data/softwares/haproxy/run/admin.sock | cut -d, -f1,2,18

Check the stats dashboard

Open in a browser:

http://10.0.1.10:8404/stats

Test a real connection through the VIP

mysql -h 10.0.1.10 -P 3306 -u youruser -p -e "SELECT @@hostname;"

Force a failover and watch it happen

On the current MASTER node:

sudo systemctl stop haproxy

Then immediately re-check the VIP on both nodes:

ip addr show eth0 | grep 10.0.1.10

Within 2–6 seconds (roughly interval × fall), the VIP should disappear from node A and appear on node B. Re-run the MySQL test command against 10.0.1.10 — it should keep working without any change on the client side. Then restart HAProxy on node A and confirm the VIP returns (since node A has the higher priority).

Troubleshooting and common pitfalls

  • VIP doesn’t move on failure — check journalctl -u keepalived on both nodes. The most common cause is a mismatched virtual_router_id, a firewall blocking VRRP (protocol 112), or the wrong interface name.
  • HAProxy won’t start — always run the -c config check first; it will point at the exact line with a syntax error, before systemd ever gets involved.
  • Health checks always fail — confirm the haproxy_check MySQL user exists on every node in the cluster, and that nothing (firewall, security group, bind-address) is blocking the HAProxy nodes from reaching MySQL on the check port.
  • Split-brain (both nodes think they’re MASTER) — almost always a network partition between the nodes preventing VRRP adverts from arriving. Make sure both nodes are on the same broadcast domain and that no security group or NIC-level filter is blocking multicast/VRRP traffic.
  • VIP flaps under load — increase fall/rise slightly, or check whether the chk_haproxy script itself is timing out under load.

Conclusion

You now have two MySQL clusters — on 3306 and 3308 — sitting behind a single virtual IP, load balanced by HAProxy, with Keepalived handling automatic failover between two independent HAProxy nodes. Nothing in this setup depends on a single server staying up, and every piece — HAProxy, Keepalived, the VIP — has been tested end to end, not just configured and assumed to work.

From here, the next useful additions are usually: a read_only-aware health check if your cluster has a single enforced writable node, TLS termination at the HAProxy layer if clients connect over an untrusted network, and centralized log shipping from both nodes so failover events show up somewhere you're actually watching.

HAProxy + Keepalived HA Guide for MySQL Clusters (2026) Step-by-step guide to a highly available MySQL setup with HAProxy and Keepalived: dual clusters, a floating VIP, and tested automatic failover.

***🐧 Linux Server Configuration — Complete Administrator’s Guide (Beginner → Advanced → Production)***

***🏆 Ultimate DevOps & SRE Learning Hub (2026 Edition) — 100% Free, Real-World Knowledge***

***☸️ Kubernetes & 🐳 Docker Mastery Hub (2026 Edition)***

***🏆 DevOps/SRE, Linux Admin Interview Preparation Hub (2026 Edition) : 500+ Questions from Linux to SRE***

🌟 Final Note

This single page is designed to be:

  • 📌 Bookmarked
  • 📌 Shared
  • 📌 Used daily

Thank you for reading! 😊🚀

If you’re a Linux admin, DevOps engineer, cloud engineer, or SRE — this page is your personal technical library.

👏 If it helped you, clap & share 💬 Drop a comment if you want a topic-wise PDF or roadmap next

Happy Learning & Troubleshooting! 🚀


메타데이터
post_id
46fc042e9eca
slug
how-to-build-a-highly-available-mysql-setup-with-haproxy-and-keepalived-46fc042e9eca
url
https://medium.com/beyond-localhost/how-to-build-a-highly-available-mysql-setup-with-haproxy-and-keepalived-46fc042e9eca
canonical_url
https://medium.com/beyond-localhost/how-to-build-a-highly-available-mysql-setup-with-haproxy-and-keepalived-46fc042e9eca
author_url
https://medium.com/@tushar.jadhav29
status
ok
fetched_at
2026-07-16 09:26:33