Practical guide: active‑passive failover between two VPS with Keepalived, HAProxy & lsyncd for synchronized uploads

By · Published on · 10 min read

Red network cable in the foreground with blurred rack servers in the background
Network infrastructure and servers — ideal to illustrate high availability and failover Photo: blickpixel / Pixabay

How to set up active‑passive failover between two VPS using Keepalived, HAProxy and lsyncd for synchronized uploads. This guide describes the topology, prerequisites and a step‑by‑step configuration. You will end up with a floating VIP via VRRP, HAProxy as the traffic entrypoint and lsyncd to replicate uploads nearly in real time over SSH. The focus is on simplicity and automatic node failover.

What is active‑passive failover with Keepalived, HAProxy and lsyncd?

Active‑passive failover uses two VPS: one serves traffic (MASTER) while the other stands by (BACKUP). Keepalived moves a virtual IP (VIP) between nodes using VRRP. HAProxy listens on the VIP and forwards to the local app. lsyncd replicates uploads from the MASTER to the BACKUP over SSH.

This architecture reduces downtime while keeping uploads consistent. It's straightforward and doesn't require an orchestrator. Below are each component's role and key considerations.

Component Role Notes
Keepalived (VRRP) Moves the VIP between VPS Unicast in cloud; priority; healthchecks; ARP/GARP
HAProxy HTTP/HTTPS entrypoint on the VIP App health check; ip_nonlocal_bind
lsyncd (rsync/SSH) Replicates uploads MASTER → BACKUP SSH keys; exclusions; avoid split‑brain
Application Serves requests locally on the active node Uploads in a dedicated directory; permissions
Firewall/Kernel Allows VRRP/VIP announcements arp_announce/ignore; routes; journald/logrotate

Which VPS prerequisites and limitations should I check?

Verify both VPS share the same subnet or support a floating IP/VIP. Some providers block multicast and gratuitous ARP. When there's no common L2, use VRRP in unicast mode and validate VIP announcement.

If the provider blocks ARP/VRRP, VIP failover won't work. An alternative is automating DNS with a low TTL, accepting DNS propagation trade‑offs. Ensure SSH keys between nodes, synchronized time (NTP), and a dedicated uploads directory.

To prepare the system and avoid kernel or logging bottlenecks, see the Debian VPS production checklist: Debian VPS checklist for production. For journald retention and compression policies, see: journald retention & compression guide.

  • Two VPS in same subnet or support for VIP/floating IP
  • Root SSH access to install and configure packages
  • Clock synced (chrony or systemd‑timesyncd)
  • Dedicated uploads directory (e.g. /var/www/app/uploads)
  • Ports: HTTP/HTTPS for HAProxy; VRRP unicast between nodes
  • Log rotation to avoid filling disk

Prepare users, directories and SSH keys for lsyncd

Create a restricted sync user, set permissions on the uploads directory and exchange SSH keys. Back up the uploads directory before automating synchronization.

In the example, node A = 10.0.0.10 (preferred MASTER) and node B = 10.0.0.11 (BACKUP). Adjust IPs, paths and usernames to your environment.

# Em AMBAS as VPS (A e B):
sudo adduser --system --group --home /var/sync sync
sudo usermod -aG www-data sync

# Diretório de uploads da aplicação (se já existir, apenas ajuste permissões):
sudo mkdir -p /var/www/app/uploads
sudo chown -R www-data:www-data /var/www/app/uploads
sudo chmod 2755 /var/www/app/uploads  # setgid para manter o grupo

# Gerar chave no nó A e autorizar no nó B (para rsync via SSH):
sudo -u sync mkdir -p /var/sync/.ssh && sudo chown -R sync:sync /var/sync/.ssh && sudo chmod 700 /var/sync/.ssh
sudo -u sync ssh-keygen -t ed25519 -C sync@vpsA -f /var/sync/.ssh/id_ed25519 -N ""
ssh-copy-id -i /var/sync/.ssh/id_ed25519.pub [email protected]

# Segurança no nó B (opcional avançado): restringir comandos no authorized_keys
# command="rrsync /var/www/app/uploads",no-agent-forwarding,no-port-forwarding,no-pty,no-user-rc,no-X11-forwarding ssh-ed25519 AAAA... sync@vpsA

How to configure Keepalived (VRRP) in unicast with a floating VIP

Install Keepalived, enable sysctl settings for ARP and ip_nonlocal_bind, and create a VRRP instance per node. Use a higher priority on the preferred primary. A vrrp_script should reduce priority if HAProxy stops.

Use notify scripts to control lsyncd: on MASTER perform a warm‑up rsync then start lsyncd; on BACKUP/FAULT stop lsyncd. Adjust IPs, interface and VIP to match your setup.

# Em AMBAS as VPS (ajuste interface e IPs):
# /etc/sysctl.d/99-vrrp.conf
net.ipv4.ip_nonlocal_bind=1
net.ipv4.conf.all.arp_ignore=1
net.ipv4.conf.all.arp_announce=2
net.ipv4.conf.default.arp_ignore=1
net.ipv4.conf.default.arp_announce=2

# Aplicar sysctl
# sysctl --system

# Instalar serviços essenciais
# apt-get update && apt-get install -y keepalived haproxy lsyncd

# /etc/keepalived/keepalived.conf (exemplo para o nó A - MASTER preferencial)
vrrp_script chk_haproxy {
    script "/usr/bin/pgrep -x haproxy"
    interval 2
    fall 2
    rise 2
}

vrrp_instance VI_1 {
    state MASTER
    interface eth0
    virtual_router_id 51
    priority 150                 # nó A mais prioritário
    advert_int 1

    authentication {
        auth_type PASS
        auth_pass 7c0m53gr3t
    }

    unicast_src_ip 10.0.0.10
    unicast_peer {
        10.0.0.11
    }

    track_script {
        chk_haproxy
    }

    virtual_ipaddress {
        10.0.0.50/24 dev eth0 label eth0:vip
    }

    notify_master "/etc/keepalived/notify.sh master"
    notify_backup "/etc/keepalived/notify.sh backup"
    notify_fault  "/etc/keepalived/notify.sh fault"
}

# No nó B, mude:
# state BACKUP
# priority 100
# unicast_src_ip 10.0.0.11
# unicast_peer { 10.0.0.10 }

# notify.sh para iniciar/parar o lsyncd conforme o papel do nó
# /etc/keepalived/notify.sh
#-----------------------------------
#!/usr/bin/env bash
set -euo pipefail
ROLE="$1"  # master|backup|fault
PEER_IP="10.0.0.11"   # do ponto de vista do nó A; ajuste no B
SRC_DIR="/var/www/app/uploads"
DST="sync@${PEER_IP}:${SRC_DIR}"
LOGTAG="keepalived-notify"

case "$ROLE" in
  master)
    logger -t "$LOGTAG" "MASTER: warm-up rsync -> $DST"
    rsync -az --inplace --partial "$SRC_DIR/" "$DST/" || true
    systemctl restart lsyncd
    ;;
  backup|fault)
    logger -t "$LOGTAG" "${ROLE^^}: stopping lsyncd"
    systemctl stop lsyncd || true
    ;;
  *)
    logger -t "$LOGTAG" "unknown role: $ROLE" ;;
esac
#-----------------------------------
# chmod 750 /etc/keepalived/notify.sh && chown root:root /etc/keepalived/notify.sh
# systemctl enable --now keepalived

Configure HAProxy to serve on the VIP

HAProxy binds to 0.0.0.0; when the VIP is on the node, client traffic arrives there. The backend health check supports Keepalived's vrrp_script. Replace port 8080 with your app's port.

For TLS and ACME you can extend this later. See a related walkthrough: HAProxy with Let's Encrypt & healthchecks.

# /etc/haproxy/haproxy.cfg (mínimo funcional)

global
    log /dev/log local0
    maxconn 2000
    daemon

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

frontend fe_http
    bind 0.0.0.0:80
    http-request set-header X-Forwarded-Proto http
    default_backend be_app

backend be_app
    option httpchk GET /health
    server app1 127.0.0.1:8080 check

# Valide e habilite
# haproxy -c -f /etc/haproxy/haproxy.cfg && systemctl enable --now haproxy

How to synchronize uploads only on the MASTER with lsyncd

lsyncd watches for changes and triggers rsync over SSH from the MASTER to the BACKUP. To avoid split‑brain, lsyncd must run only on the node that holds the VIP. The Keepalived notify.sh handles this.

Use exclusions for temporary files and enable --inplace to reduce duplicate writes. Be careful with --delete: only enable it after validating sync direction.

-- /etc/lsyncd/lsyncd.lua
settings {
    logfile    = "/var/log/lsyncd.log",
    statusFile = "/var/run/lsyncd.status",
    nodaemon   = false,
    maxDelays  = 5,
}

default.rsyncssh {
    source    = "/var/www/app/uploads",
    host      = "[email protected]",   -- destino (nó B)
    targetdir = "/var/www/app/uploads",
    rsync = {
        archive  = true,
        compress = true,
        verbose  = true,
        _extra   = { "--inplace", "--partial" }
        -- Para espelhamento completo, adicione: "--delete" (use com cautela)
    },
    delay = 2,
    excludeFrom = "/etc/lsyncd.exclude"
}

-- /etc/lsyncd.exclude (exemplo)
-- *.tmp
-- .*swp
-- .DS_Store

-- Habilite o serviço (start/stop será controlado pelo Keepalived):
-- systemctl enable lsyncd

How to test failover and confirm it worked

Check which node owns the VIP, perform a test upload and simulate service failure. HAProxy should expose a simple /health route. The VIP should migrate within seconds when the health check fails.

After things stabilize, verify the test file appeared on the BACKUP node. Monitor logs to understand failover and sync timings.

# 1) Quem é o MASTER (deve mostrar 10.0.0.50 no nó ativo)
ip -br a | grep 10.0.0.50 || echo "VIP não está neste nó"

# 2) Teste HTTP pelo VIP
curl -sS http://10.0.0.50/health || echo "sem resposta"

# 3) Upload/arquivo de teste no MASTER e checar no BACKUP
TS=$(date +%s)
echo "ok-$TS" | sudo -u www-data tee /var/www/app/uploads/test-$TS.txt >/dev/null
ssh [email protected] "ls -l /var/www/app/uploads/test-$TS.txt"

# 4) Simule falha do HAProxy no MASTER e observe failover
sudo systemctl stop haproxy
sleep 3
curl -sS http://10.0.0.50/health || echo "sem resposta"

# 5) Logs úteis
journalctl -u keepalived -n 50 --no-pager
journalctl -u lsyncd -n 50 --no-pager

Best practices, observability and common issues

Monitor logs, disk usage and synchronization latency. Rotate logs so VRRP and lsyncd won't be affected. Keep healthchecks simple and reliable. Prevent the application from accepting writes on the BACKUP.

If something breaks, inspect network, ARP/VRRP and uploads directory permissions. For split‑brain risks with data, also see this PostgreSQL failover perspective: avoiding split‑brain in PostgreSQL failover.

  • Use fast healthchecks (/health) in HAProxy and a simple track_script
  • Keep higher priority on the MASTER; do not change virtual_router_id
  • Open firewall for VRRP unicast between the VPS IPs
  • Check uploads directory permissions (setgid and application group)
  • Rotate systemd‑journald logs to avoid full disks
  • Document runbooks and test failover periodically

Frequently asked questions

Do both VPS need to be on the same network for the VIP to work?

Yes. In most cases, both VPS must share the same subnet or the provider must offer a floating IP feature. Without a common L2 or support for ARP/VRRP, the VIP won't fail over between nodes. The alternative is automating DNS with a low TTL, accepting DNS propagation delays.

How do I avoid split‑brain for uploads with lsyncd?

Run lsyncd only on the MASTER node, controlled by Keepalived's notify script. Perform a warm‑up rsync when taking the MASTER role and validate sync direction before enabling --delete. Prevent the application from accepting writes on both nodes simultaneously.

What is the typical sync latency with lsyncd?

Sync is near real‑time but depends on I/O, CPU, network and file count. Changes are aggregated with small delays (for example, 2–5 seconds) for efficiency. During peaks or initial syncs with many files, completion can take longer before steady state.

Does this work with IPv6 for the VIP?

It can, if the provider supports announcing an IPv6 VIP and the network accepts Neighbor Advertisements (the IPv6 equivalent of ARP). In many clouds it's easier to use the provider's floating IP. Adjust Keepalived and HAProxy for IPv6 addresses as needed.

Can I use NFS or shared storage instead of lsyncd?

Yes, but shared storage adds complexity and potential single points of failure. NFS needs tuning, proper firewall rules and locking tests. lsyncd is simpler for occasional uploads and small‑to‑medium directories. For large volumes or concurrent writes, evaluate distributed storage solutions.

Want a topology review before going to production?

Send a sketch of your network (IPs, subnet, desired VIP) and your upload requirements. An external review helps spot weak points and define realistic failover tests.

Discuss your scenario

See also