Tools: From Zero to Live: How I Deploy 5 Apps on a Single VPS - Full Analysis

Tools: From Zero to Live: How I Deploy 5 Apps on a Single VPS - Full Analysis

From Zero to Live: How I Deploy 5 Apps on a Single $5 VPS

Why One Server?

The Hardware

The Architecture

Step 1: Install the Basics

Step 2: SSL Certificate (Free)

Step 3: App #1 — Node.js Web App

Keep It Alive with systemd

Step 4: Nginx Reverse Proxy

Step 5: Add a Static Site (Hugo Blog)

Step 6: Security Basics

Firewall

Fail2Ban

Rate Limiting in Nginx

Security Headers

Cost Breakdown

What I'd Do Differently

Go Forth and Deploy Yes, you read that right. Five separate applications running on one cheap server. Here's my exact setup. When I started building side projects, I made the mistake every beginner makes: One Heroku app per project = $7-14/month each After 4 projects, I was paying $56/month for apps that had zero users. That's when I discovered the magic of a single VPS: Here's how I run 5 applications on one server. I'm using a Tencent Cloud Lighthouse instance (similar to DigitalOcean Droplet): Any provider works: DigitalOcean, Linode, Hetzner, Vultr — pick whatever has a data center close to your users. If you don't have a server yet, DigitalOcean gives you $200 in credits for new accounts. That's 40 months free. The secret sauce: Nginx routes traffic by URL path to different backend ports. Each app thinks it has its own server. That's it. That's your entire stack. Every app needs HTTPS. Here's the easiest way: Certbot auto-configures Nginx for HTTPS and sets up auto-renewal. Your certificates will never expire. Pro tip: Put Cloudflare in front of your domain for extra DDoS protection and a free CDN. Set SSL mode to "Full (Strict)" in Cloudflare dashboard. Let's say you have a simple Express app: Create /etc/systemd/system/yourapp.service: Critical detail: Use the FULL path to node from nvm (which node gives you this). Systemd doesn't load your shell profile. This is where the magic happens. Edit your Nginx config: Result: yourdomain.com/ serves App 1, yourdomain.com/signal/ serves App 2. Both on HTTPS, both on one server. For static sites (blog, docs, landing pages), no need for a Node.js process: Static sites consume almost zero RAM. You could host 50 of them and not notice. You're exposed to the internet now. Lock it down: Automatically bans IPs after too many failed login attempts. After running all 5 apps for 6 months: I'm using half my resources. Room for plenty more apps. Compare that to running 5 separate Heroku dynamos ($7 × 5 = $35/month), and you're saving 85%. You don't need Kubernetes. You don't need AWS. You don't need a DevOps team. Everything I run today started exactly like this. Small, cheap, imperfect — but live and learning. What are you building? Drop a comment — I'd love to hear what you're working on. New here? Follow @armorbreak for more practical guides like this. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Code Block

Copy

CPU: 2 cores RAM: 4GB Disk: 60GB SSD Bandwidth: 1TB/month Cost: ~$5/month CPU: 2 cores RAM: 4GB Disk: 60GB SSD Bandwidth: 1TB/month Cost: ~$5/month CPU: 2 cores RAM: 4GB Disk: 60GB SSD Bandwidth: 1TB/month Cost: ~$5/month ┌─────────────────┐ │ Cloudflare │ │ (CDN + HTTPS) │ └────────┬────────┘ │ ┌────────▼────────┐ │ Nginx │ │ (Reverse Proxy)│ └───────┬┬───────┘ │ │ ┌──────────────────┘ └──────────────────┐ │ │ ┌──────▼──────┐ ┌────────▼────────┐ │ App 1 │ │ App 2 │ │ (:3000) │ │ (:3001) │ │ Main site │ │ Signal service │ └─────────────┘ └─────────────────┘ ┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐ │ App 3 │ │ App 4 │ │ App 5 │ │ (:3099) │ │ Static │ │ Room UI │ │ Formatter │ │ Blog/Hugo │ │ Dashboard │ └─────────────┘ └─────────────┘ └─────────────┘ ┌─────────────────┐ │ Cloudflare │ │ (CDN + HTTPS) │ └────────┬────────┘ │ ┌────────▼────────┐ │ Nginx │ │ (Reverse Proxy)│ └───────┬┬───────┘ │ │ ┌──────────────────┘ └──────────────────┐ │ │ ┌──────▼──────┐ ┌────────▼────────┐ │ App 1 │ │ App 2 │ │ (:3000) │ │ (:3001) │ │ Main site │ │ Signal service │ └─────────────┘ └─────────────────┘ ┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐ │ App 3 │ │ App 4 │ │ App 5 │ │ (:3099) │ │ Static │ │ Room UI │ │ Formatter │ │ Blog/Hugo │ │ Dashboard │ └─────────────┘ └─────────────┘ └─────────────┘ ┌─────────────────┐ │ Cloudflare │ │ (CDN + HTTPS) │ └────────┬────────┘ │ ┌────────▼────────┐ │ Nginx │ │ (Reverse Proxy)│ └───────┬┬───────┘ │ │ ┌──────────────────┘ └──────────────────┐ │ │ ┌──────▼──────┐ ┌────────▼────────┐ │ App 1 │ │ App 2 │ │ (:3000) │ │ (:3001) │ │ Main site │ │ Signal service │ └─────────────┘ └─────────────────┘ ┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐ │ App 3 │ │ App 4 │ │ App 5 │ │ (:3099) │ │ Static │ │ Room UI │ │ Formatter │ │ Blog/Hugo │ │ Dashboard │ └─────────────┘ └─────────────┘ └─────────────┘ # Update system sudo apt update && sudo apt upgrade -y # Install Nginx sudo apt install nginx -y # Install Node.js (via nvm) curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash nvm install --lts # Install Git sudo apt install git -y # Update system sudo apt update && sudo apt upgrade -y # Install Nginx sudo apt install nginx -y # Install Node.js (via nvm) curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash nvm install --lts # Install Git sudo apt install git -y # Update system sudo apt update && sudo apt upgrade -y # Install Nginx sudo apt install nginx -y # Install Node.js (via nvm) curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash nvm install --lts # Install Git sudo apt install git -y # Install Certbot sudo apt install certbot python3-certbot-nginx -y # Get certificate (replace with your domain) sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com # Install Certbot sudo apt install certbot python3-certbot-nginx -y # Get certificate (replace with your domain) sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com # Install Certbot sudo apt install certbot python3-certbot-nginx -y # Get certificate (replace with your domain) sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com // server.js const express = require('express'); const app = express(); const PORT = 3000; app.get('/api/health', (req, res) => { res.json({ status: 'ok', uptime: process.uptime() }); }); app.listen(PORT, '127.0.0.1', () => { console.log(`App running on port ${PORT}`); }); // server.js const express = require('express'); const app = express(); const PORT = 3000; app.get('/api/health', (req, res) => { res.json({ status: 'ok', uptime: process.uptime() }); }); app.listen(PORT, '127.0.0.1', () => { console.log(`App running on port ${PORT}`); }); // server.js const express = require('express'); const app = express(); const PORT = 3000; app.get('/api/health', (req, res) => { res.json({ status: 'ok', uptime: process.uptime() }); }); app.listen(PORT, '127.0.0.1', () => { console.log(`App running on port ${PORT}`); }); [Unit] Description=Your Application After=network.target [Service] Type=simple User=ubuntu WorkingDirectory=/home/ubuntu/your-app ExecStart=/home/ubuntu/.nvm/versions/node/v22.22.1/bin/node server.js Restart=always RestartSec=10 Environment=NODE_ENV=production [Install] WantedBy=multi-user.target [Unit] Description=Your Application After=network.target [Service] Type=simple User=ubuntu WorkingDirectory=/home/ubuntu/your-app ExecStart=/home/ubuntu/.nvm/versions/node/v22.22.1/bin/node server.js Restart=always RestartSec=10 Environment=NODE_ENV=production [Install] WantedBy=multi-user.target [Unit] Description=Your Application After=network.target [Service] Type=simple User=ubuntu WorkingDirectory=/home/ubuntu/your-app ExecStart=/home/ubuntu/.nvm/versions/node/v22.22.1/bin/node server.js Restart=always RestartSec=10 Environment=NODE_ENV=production [Install] WantedBy=multi-user.target sudo systemctl enable yourapp sudo systemctl start yourapp sudo systemctl status yourapp # Should show "active (running)" sudo systemctl enable yourapp sudo systemctl start yourapp sudo systemctl status yourapp # Should show "active (running)" sudo systemctl enable yourapp sudo systemctl start yourapp sudo systemctl status yourapp # Should show "active (running)" server { listen 443 ssl http2; server_name yourdomain.com; ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem; # Main app → port 3000 location / { 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; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # Second app → port 3001 location /signal/ { proxy_pass http://127.0.0.1:3001/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } server { listen 443 ssl http2; server_name yourdomain.com; ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem; # Main app → port 3000 location / { 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; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # Second app → port 3001 location /signal/ { proxy_pass http://127.0.0.1:3001/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } server { listen 443 ssl http2; server_name yourdomain.com; ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem; # Main app → port 3000 location / { 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; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # Second app → port 3001 location /signal/ { proxy_pass http://127.0.0.1:3001/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } sudo nginx -t # Check syntax sudo systemctl reload nginx sudo nginx -t # Check syntax sudo systemctl reload nginx sudo nginx -t # Check syntax sudo systemctl reload nginx # Install Hugo hugo_version="0.146.0" curl -L -o hugo.tar.gz \ "https://github.com/gohugoio/hugo/releases/download/v${hugo_version}/hugo_${hugo_version}_linux-amd64.tar.gz" tar -xzf hugo.tar.gz && sudo mv hugo /usr/local/bin/ # Create site hugo new site my-blog && cd my-blog git submodule add https://github.com/adityatelange/hugo-PaperMod themes/PaperMod # Configure (hugo.toml): # baseURL = "https://yourdomain.com/blog/" # theme = 'PaperMod' # Write a post hugo new content posts/my-first-post.md # Build hugo --minify # Output is in public/ # Install Hugo hugo_version="0.146.0" curl -L -o hugo.tar.gz \ "https://github.com/gohugoio/hugo/releases/download/v${hugo_version}/hugo_${hugo_version}_linux-amd64.tar.gz" tar -xzf hugo.tar.gz && sudo mv hugo /usr/local/bin/ # Create site hugo new site my-blog && cd my-blog git submodule add https://github.com/adityatelange/hugo-PaperMod themes/PaperMod # Configure (hugo.toml): # baseURL = "https://yourdomain.com/blog/" # theme = 'PaperMod' # Write a post hugo new content posts/my-first-post.md # Build hugo --minify # Output is in public/ # Install Hugo hugo_version="0.146.0" curl -L -o hugo.tar.gz \ "https://github.com/gohugoio/hugo/releases/download/v${hugo_version}/hugo_${hugo_version}_linux-amd64.tar.gz" tar -xzf hugo.tar.gz && sudo mv hugo /usr/local/bin/ # Create site hugo new site my-blog && cd my-blog git submodule add https://github.com/adityatelange/hugo-PaperMod themes/PaperMod # Configure (hugo.toml): # baseURL = "https://yourdomain.com/blog/" # theme = 'PaperMod' # Write a post hugo new content posts/my-first-post.md # Build hugo --minify # Output is in public/ location /blog { alias /path/to/my-blog/public; index index.html; try_files $uri $uri/ /blog/index.html; } location /blog { alias /path/to/my-blog/public; index index.html; try_files $uri $uri/ /blog/index.html; } location /blog { alias /path/to/my-blog/public; index index.html; try_files $uri $uri/ /blog/index.html; } sudo ufw allow 22/tcp # SSH sudo ufw allow 80/tcp # HTTP sudo ufw allow 443/tcp # HTTPS sudo ufw enable sudo ufw allow 22/tcp # SSH sudo ufw allow 80/tcp # HTTP sudo ufw allow 443/tcp # HTTPS sudo ufw enable sudo ufw allow 22/tcp # SSH sudo ufw allow 80/tcp # HTTP sudo ufw allow 443/tcp # HTTPS sudo ufw enable sudo apt install fail2ban -y sudo systemctl enable fail2ban sudo apt install fail2ban -y sudo systemctl enable fail2ban sudo apt install fail2ban -y sudo systemctl enable fail2ban # In http block: limit_req_zone $binary_remote_addr zone=general rate=10r/s; limit_req_zone $binary_remote_addr zone=login rate=5r/m; # In location block: location /api/auth/ { limit_req zone=login burst=5 nodelay; proxy_pass http://127.0.0.1:3000; } # In http block: limit_req_zone $binary_remote_addr zone=general rate=10r/s; limit_req_zone $binary_remote_addr zone=login rate=5r/m; # In location block: location /api/auth/ { limit_req zone=login burst=5 nodelay; proxy_pass http://127.0.0.1:3000; } # In http block: limit_req_zone $binary_remote_addr zone=general rate=10r/s; limit_req_zone $binary_remote_addr zone=login rate=5r/m; # In location block: location /api/auth/ { limit_req zone=login burst=5 nodelay; proxy_pass http://127.0.0.1:3000; } add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; Total RAM: 4GB Used: ~2.2GB (55%) Free: ~1.8GB Total Disk: 60GB Used: ~37GB (62%) Free: ~23GB CPU Load: 0.05-0.15 (basically idle) Total RAM: 4GB Used: ~2.2GB (55%) Free: ~1.8GB Total Disk: 60GB Used: ~37GB (62%) Free: ~23GB CPU Load: 0.05-0.15 (basically idle) Total RAM: 4GB Used: ~2.2GB (55%) Free: ~1.8GB Total Disk: 60GB Used: ~37GB (62%) Free: ~23GB CPU Load: 0.05-0.15 (basically idle) - $5/month total (not per app) - Full control over everything - Skills you'll use forever (Linux, Nginx, systemd) - Unlimited apps (within resource limits) - Set up monitoring from day one. I didn't learn about UptimeRobot until an app was down for 3 days without me noticing. - Use Docker from the start. Makes deploying new apps much easier (though raw Node.js works fine for simple cases). - Automate deployments. Right now I SSH in and pull manually. A simple CI/CD pipeline would save time. - Back up automatically. I learned this the hard way after losing a database. Now I have daily automated backups to object storage. - Basic Linux knowledge - A willingness to Google error messages