Practical guide: active‑passive failover between two VPS with Keepalived, HAProxy & lsyncd for synchronized uploads
By Kevin Marcondes · Published on · 10 min read
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 scenarioSee also
- Guide: technical checklist to prepare a Debian VPS with Docker Compose for PHP apps in production
- Practical guide to VPS sizing and PHP‑FPM tuning for Magento/OpenMage stores with seasonal peaks: from sizing to load testing
- Practical guide: rebuild PostgreSQL indexes in production using CREATE INDEX CONCURRENTLY and partial indexes (safe step‑by‑step)
- Practical guide to implementing exponential retries and circuit breakers in ERP–marketplace integrations