How I Host 5 Projects on One $5 VPS (Without Everything Breaking)
One server. Five projects. Zero downtime (mostly). Here's my setup.
The Scenario
I'm a solo developer running multiple side projects:
- AgentVote — Web app (Node.js, port 3000)
- CryptoSignal — API service (Node.js, port 3001)
- Text Formatter — Tool page (Node.js, port 3099)
- Blog — Hugo static site (Node.js, port 3098)
- Automation Bot — Background worker (cron-based)
All on one $5/month VPS with 2 vCPU and 4GB RAM.
Here's how I make it work without everything falling over when one thing hogs resources.
The Stack
┌─────────────────────────┐
│ Cloudflare CDN (HTTPS)│
│ → SSL termination │
│ → DDoS protection │
└───────────┬─────────────┘
│
┌───────────▼─────────────┐
│ Nginx (port 80/443) │
│ Reverse proxy + routing │
└──┬──────┬──────┬────────┘
│ │ │
┌────────▼┐ ┌──▼───┐ ┌▼────────┐
│ :3000 │ │:3099 │ │ :3098 │
│AgentVote│ │Format│ │ Blog │
└────────┘ └──────┘ └─────────┘
┌────────┐ ┌──────────────────┐
│ :3001 │ │ Cron / Background │
│Signal │ │ (no port needed) │
└────────┘ └──────────────────┘
Step 1: Nginx as Traffic Cop
Nginx sits in front of everything. It handles:
- SSL (terminates HTTPS from Cloudflare)
-
Routing (sends
/blog/*to blog server,/signal/*to signal API, etc.) - Rate limiting (protects against abuse)
- Static files (serves directly without hitting Node)
Key config pattern:
# Main app
location /vote/ {
proxy_pass http://127.0.0.1:3000/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# API service
location /signal/ {
proxy_pass http://127.0.0.1:3001/;
# Rate limit API calls
limit_req zone=api burst=20 nodelay;
}
# Static tool
location = /format {
return 301 /format/;
}
location /format/ {
proxy_pass http://127.0.0.1:3099/;
}
# Blog
location /blog/ {
proxy_pass http://127.0.0.1:3098/;
}
Step 2: Process Management
Each Node.js process needs to stay alive. I use two layers:
Layer 1: Guardian Script
Each project has a guardian.sh:
#!/bin/bash
PROJECT_DIR="/root/data/disk/projects/my-app"
PORT=3000
if ! ss -tlnp | grep -q ":${PORT}"; then
cd "$PROJECT_DIR"
export PATH="/root/.nvm/current/bin:$PATH"
nohup node server.js >> /var/log/my-app.log 2>&1 &
fi
Layer 2: Crontab (every 5 minutes)
*/5 * * * * /root/data/disk/projects/my-app/guardian.sh
*/5 * * * * /root/data/disk/projects/signal/guardian.sh
*/5 * * * * /root/data/disk/projects/text-formatter/guardian.sh
*/5 * * * * /root/data/disk/projects/alexchen-blog/guardian.sh
If any process dies, it's back within 5 minutes.
Why not PM2/systemd? They're great, but guardian scripts are simpler to debug when you're SSH'd into a remote server at 2 AM trying to figure out why everything is down.
Step 3: Memory Management (The Hard Part)
4GB RAM sounds like a lot until you run 5 Node processes + Nginx + Playwright browser instances + cron jobs.
Current Memory Budget:
| Process | Typical Usage | Max |
|---|---|---|
| AgentVote (app) | ~80MB | ~200MB |
| CryptoSignal | ~60MB | ~150MB |
| Text Formatter | ~40MB | ~80MB |
| Blog (static) | ~30MB | ~50MB |
| Nginx | ~20MB | ~50MB |
| Automation bot | ~100MB | ~500MB |
| Playwright (browser) | ~200MB | ~800MB |
| System/OS | ~400MB | ~600MB |
| Total | ~930MB | ~2430MB |
I have about 1.5GB headroom normally. But when Playwright launches a Chromium instance for web scraping, memory spikes fast.
What I Do:
// In automation code: always close browsers explicitly
async function scrape(url) {
let browser;
try {
browser = await chromium.launch({
headless: true,
// Limit memory usage
args: ['--max-old-space-size=256']
});
const page = await browser.newPage();
// ... do work ...
return result;
} finally {
// ALWAYS close, even if error
if (browser) await browser.close();
}
}
And a safety net:
# If free memory drops below 200MB, kill the oldest non-critical process
FREE_MB=$(free -m | awk '/Mem:/ {print $7}')
if [ "$FREE_MB" -lt 200 ]; then
pkill -f "playwright" || true
logger "Low memory alert: ${FREE_MB}MB free, killed playwright"
fi
Step 4: Disk Space
60GB fills up faster than you'd expect with logs, git repos, and node_modules.
My disk hygiene rules:
# 1. Rotate logs (keep 7 days, max 50M each)
# In each project's server.js:
const logFile = fs.createWriteStream('./app.log', { flags: 'a' });
// External logrotate handles rotation
# 2. Clean npm cache weekly
0 3 * * 0 npm cache clean --force
# 3. Monitor disk usage
df -h | awk '$NF=="/"{if($5+0 > 90) print "WARNING: Disk at " $5}'
Pro tip: du -sh /* 2>/dev/null | sort -hr | head -10 tells you exactly what's eating space.
Step 5: Monitoring (Don't Fly Blind)
You can't fix what you don't know is broken.
Health Check Endpoint
Each app exposes GET /health:
app.get('/health', (req, res) => {
res.json({
status: 'ok',
uptime: process.uptime(),
memory: process.memoryUsage(),
timestamp: Date.now()
});
});
Uptime Monitor
External monitoring via UptimeRobot (free tier):
- Checks every 5 minutes
- Alerts to email if down
- Public status page for users
Internal Watchdog
A script that checks if all expected ports are listening:
#!/bin/bash
PORTS=(3000 3001 3098 3099)
for PORT in "${PORTS[@]}"; do
if ! ss -tlnp | grep -q ":${PORT}"; then
echo "ALERT: Port $PORT is DOWN"
# Send notification (Feishu/Discord/webhook)
fi
done
When This Setup Breaks (And It Does)
Common Failures:
-
OOM Killer takes out Node process
- Fix: Reduce memory usage or add swap file
-
Disk full → database writes fail
- Fix: Automated cleanup scripts + alerts at 85%
-
Nginx config syntax error → nothing works
- Fix: Always test with
nginx -tbefore reload
- Fix: Always test with
-
SSL cert expires
- Fix: Let's Encrypt auto-renewal via certbot timer
-
Git pull fails because of merge conflict
- Fix: Don't pull on production. Deploy from clean clone.
Recovery Procedure:
# 1. Check what's wrong
systemctl status nginx
ss -tlnp
free -m
df -h
# 2. Restart services in order
systemctl restart nginx
# Then restart each app via guardian or manually
# 3. Verify
curl -s https://agentvote.cc/health
curl -s https://agentvote.cc/signal/api/v1/pricing
curl -s https://agentvote.cc/blog/
The Cost Breakdown
| Item | Cost/Month |
|---|---|
| VPS (2C/4G/60G) | $5 |
| Domain (agentvote.cc) | ~$1 |
| Cloudflare (CDN + DNS) | $0 |
| Let's Encrypt (SSL) | $0 |
| UptimeRobot (monitoring) | $0 |
| All software (Node, Nginx, Hugo) | $0 |
| Total | ~$6/month |
For 5 projects. That's $1.20 per project per month.
Would I Recommend This?
Pros:
- Extremely cheap
- Full control over everything
- Learn infrastructure skills
- Easy to migrate individual projects later
Cons:
- You're the DevOps team
- Single point of failure (one server = one SPOF)
- Time spent maintaining vs. building features
- Scaling means moving to separate servers eventually
Verdict: Perfect for early-stage projects with low traffic. Start here. Move to managed hosting when revenue justifies it.
What's your hosting setup? One big server? Containers? Serverless? Share your stack in the comments.
Looking for a VPS provider? I use DigitalOcean (referral link — gets you $200 credit for 60 days, helps me pay my server bill).
Top comments (0)