DEV Community

Alex Chen
Alex Chen

Posted on

How I Host 5 Projects on One $5 VPS (Without Everything Breaking)

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:

  1. AgentVote — Web app (Node.js, port 3000)
  2. CryptoSignal — API service (Node.js, port 3001)
  3. Text Formatter — Tool page (Node.js, port 3099)
  4. Blog — Hugo static site (Node.js, port 3098)
  5. 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)  │
              └────────┘ └──────────────────┘
Enter fullscreen mode Exit fullscreen mode

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/;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}'
Enter fullscreen mode Exit fullscreen mode

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()
  });
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

When This Setup Breaks (And It Does)

Common Failures:

  1. OOM Killer takes out Node process

    • Fix: Reduce memory usage or add swap file
  2. Disk full → database writes fail

    • Fix: Automated cleanup scripts + alerts at 85%
  3. Nginx config syntax error → nothing works

    • Fix: Always test with nginx -t before reload
  4. SSL cert expires

    • Fix: Let's Encrypt auto-renewal via certbot timer
  5. 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/
Enter fullscreen mode Exit fullscreen mode

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)