High Availability TODO application with Vagrant, HAProxy, Node.js, and MariaDB Galera Cluster.
graph TB
subgraph "Host Machine"
Browser["Browser<br/>:8080"]
Stats["HAProxy Stats<br/>:8404"]
end
subgraph "Frontend Network - 192.168.50.0/24"
LB["Load Balancer<br/>192.168.50.10<br/>HAProxy"]
APP1["App Server 1<br/>192.168.50.11<br/>Node.js + garbd"]
APP2["App Server 2<br/>192.168.50.12<br/>Node.js + garbd"]
end
subgraph "Backend Network - 192.168.60.0/24"
VIP["Virtual IP<br/>192.168.60.30<br/>Keepalived"]
DB1["Database 1<br/>192.168.60.21<br/>MariaDB Galera"]
DB2["Database 2<br/>192.168.60.22<br/>MariaDB Galera"]
end
Browser --> LB
Stats -.-> LB
LB -->|Round Robin| APP1
LB -->|Round Robin| APP2
APP1 <-->|rsync<br/>File Sync| APP2
APP1 --> VIP
APP2 --> VIP
VIP --> DB1
VIP --> DB2
DB1 <-->|Multi-Master<br/>Replication| DB2
Key Features:
- Load Balancing: HAProxy distributes traffic across 2 app servers
- Database HA: Galera multi-master cluster with VIP failover (Keepalived)
- File Sync: Real-time bidirectional rsync + inotify between app servers
- Arbitrators: 2 garbd instances on app servers provide quorum (4-node voting)
- Networks: Separated frontend (50.x), backend (60.x), and management (70.x)
| VM | Role | RAM | Frontend IP | Backend IP |
|---|---|---|---|---|
| lb | HAProxy + UFW | 512 MB | 192.168.50.10 | - |
| app1 | Node.js + garbd + inotify-tools + UFW | 512 MB | 192.168.50.11 | 192.168.60.11 |
| app2 | Node.js + garbd + inotify-tools + UFW | 512 MB | 192.168.50.12 | 192.168.60.12 |
| db1 | MariaDB Galera + Keepalived + rsync + UFW | 1024 MB | - | 192.168.60.21 |
| db2 | MariaDB Galera + Keepalived + rsync + UFW | 1024 MB | - | 192.168.60.22 |
Prerequisites: VirtualBox 6.1+, Vagrant 2.2+, 4GB RAM, 10GB disk
Recommended Quick Start (recommended order to ensure Galera quorum)
# Start DB VMs
vagrant up db1 db2 --provision
# Start app servers
vagrant up app1 app2 --provision
# Start load balancer
vagrant up lb --provision
# NOTE: --provision is needed the first time.Check status:
vagrant statusAccess services from the host:
- TODO App: http://localhost:8080
- HAProxy Stats: http://localhost:8404/haproxy_stats (admin/pass)
Common tasks:
# SSH
vagrant ssh app1
# Stop all VMs
vagrant halt
# Destroy all VMs
vagrant destroy -f# Service status
vagrant ssh app1 -c "sudo systemctl status todo-app"
vagrant ssh db1 -c "sudo systemctl status mariadb"
# Application logs
vagrant ssh app1 -c "tail -f /home/vagrant/app.log"
# Cluster size
vagrant ssh db1 -c "mysql -u root -e 'SHOW STATUS LIKE \"wsrep_cluster_size\";'"
# VIP location
vagrant ssh db1 -c "ip addr show eth1 | grep 192.168.60.30"
# File sync status
vagrant ssh app1 -c "sudo systemctl status upload-sync"
vagrant ssh app1 -c "sudo tail -f /var/log/upload-sync.log"# Access MySQL
vagrant ssh db1 -c "mysql -u root"
# Query todos
vagrant ssh db1 -c "mysql -u root -e 'SELECT * FROM todo_app.todos;'"The ./app folder syncs automatically. After editing server.js:
vagrant ssh app1 -c "sudo systemctl restart todo-app"
vagrant ssh app2 -c "sudo systemctl restart todo-app"App Failover:
vagrant ssh app1 -c "sudo systemctl stop todo-app"
curl http://localhost:8080 # Still works via app2
vagrant ssh app1 -c "sudo systemctl start todo-app"Database Failover:
vagrant ssh db1 -c "sudo systemctl stop mariadb"
curl http://localhost:8080 # Still works via db2 (VIP fails over)
vagrant ssh db1 -c "sudo systemctl start mariadb"File Sync:
# Upload image via web UI, then verify sync:
vagrant ssh app1 -c "ls /home/vagrant/app/public/uploads/"
vagrant ssh app2 -c "ls /home/vagrant/app/public/uploads/"