Every growing site hits the same wall: one server, one traffic spike, one outage. The classic answer is horizontal scaling: put a load balancer in front of two identical app servers, and let a private network carry the traffic between them. Three small Cloud VPS and a free vMesh network are all it takes. Here’s the whole setup.
Prerequisites
-
- Three Cloud VPS instances running Ubuntu 22.04 or newer
-
- A vMesh private network connecting them (see the setup tutorial)
-
- Administrative (root) access to each VPS
-
- A domain name pointed at the load balancer’s public IP
Why a load-balanced trio beats one big server
-
- Survive traffic spikes: two app nodes share the load; when things get hot, you add a third in minutes instead of resizing and rebooting.
-
- Deploy without downtime: update one node while the other serves traffic, then swap. No more 3 a.m. maintenance windows.
-
- No single point of panic: if an app node dies, the load balancer routes around it and your site stays up.
The topology
| Role | VPS | Exposure |
|---|---|---|
| Load balancer (Nginx, TLS) | lb-01 | Public IP + 10.0.0.1 |
| App node (your site) | web-01 | Private only, 10.0.0.2 |
| App node (identical copy) | web-02 | Private only, 10.0.0.3 |

Visitors only ever talk to the load balancer. The app nodes serve their traffic over the private network: fast, isolated, and invisible from the internet.
Step 1: Link the three VPS with vMesh
Give each VPS its private address (10.0.0.1, 10.0.0.2, 10.0.0.3) on the vMesh interface. The per-distribution walkthrough is here: How to configure your vMesh private network on Linux. Then confirm the mesh works from lb-01:
ping 10.0.0.2
ping 10.0.0.3
Step 2: Point the load balancer at your app nodes
On lb-01, define the pool once. Nginx spreads requests across both nodes and skips any that stop responding:
upstream app_pool {
server 10.0.0.2;
server 10.0.0.3;
}
server {
listen 80;
server_name yourdomain.com;
location / {
proxy_pass http://app_pool;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
sudo nginx -t && sudo systemctl reload nginx
Step 3: Lock the app nodes down to the private network
The app nodes keep internet access for updates, but nothing on the internet needs to reach them. Standard firewall practice: deny inbound by default, allow the mesh and SSH:
sudo ufw default deny incoming
sudo ufw allow in on eth1 from 10.0.0.0/24
sudo ufw allow 22/tcp
sudo ufw enable
And the everyday SSH hygiene that goes with any internet-facing VPS: keys only, no passwords:
PasswordAuthentication no
PermitRootLogin prohibit-password
On lb-01, open 80 and 443 to the world instead. It’s the only box that should ever face visitors.
Step 4: Watch it survive a failure
The best part of this setup is proving it works. Stop the app on one node:
sudo systemctl stop nginx # on web-01
Then reload your site: it still answers, served entirely by web-02. That moment, killing a server while your site stays up, is what production-grade feels like. Start web-01 again and the pool heals itself.
What you’ve built
Reload your site one more time and take stock: two servers now answer for it, traffic flows over a private network the internet cannot see, and the only machine facing visitors is the one built for it. You can lose an app node in the middle of launch day and nobody will notice. Deploys no longer need a maintenance window.
And the pattern grows with you: add web-03 to the pool with one line, move your database to its own private node, or slot a cache next to the app tier. The load balancer and the mesh stay exactly as they are.