Hosting multiple Node.js applications on a single VPS

Hosting multiple Node.js applications on a single VPS while maintaining zero downtime, strong security, and automated CI/CD requires a battle-tested architecture.

Here is the industry-standard architecture and best practices for setting this up cleanly.

architecture Overview

Rather than exposing every Node.js app on a public port directly, you route all traffic through a Reverse Proxy (Nginx) using domain names or subdomains.

Plaintext

[ Incoming Web Traffic ]
           │
           ▼
    ┌─────────────┐
    │    Nginx    │ ── (Handles SSL, Domain Routing, Port Forwarding)
    └──────┬──────┘
           │
     ┌─────┴────────────────┐
     ▼                      ▼
┌───────────────┐   ┌───────────────┐
│ App 1 (PM2)   │   │ App 2 (PM2)   │
│ Port 3000     │   │ Port 3001     │
└───────────────┘   └───────────────┘

1. Process Management: Use PM2 or Docker

Never run production Node.js apps with node index.js or npm start.

Option A: PM2 (Simplest & Most Efficient for a Single VPS)

PM2 keeps your apps alive, restarts them on server reboot, and manages environment variables.

  • Ecosystem File: Use an ecosystem.config.js file in your project root to manage settings per application:

JavaScript

module.exports = {
  apps: [
    {
      name: "alphanetix-api",
      script: "./dist/index.js",
      instances: "max",        // Uses all CPU cores (Cluster Mode)
      exec_mode: "cluster",
      env: {
        NODE_ENV: "production",
        PORT: 3000
      }
    }
  ]
};
  • Useful PM2 Commands:Bashpm2 start ecosystem.config.js pm2 save # Saves active list for automatic reboot recovery pm2 startup # Configures systemd to start PM2 on boot pm2 status # Visual dashboard of running apps

Option B: Docker + Docker Compose (Best for Isolation)

If applications require different Node.js versions or complex background services, containerize each app and manage them with a single docker-compose.yml.

2. Reverse Proxy & SSL: Nginx + Certbot

Nginx listens on ports 80/443 and routes requests to the internal ports where your Node.js apps live.

Sample Nginx Configuration (/etc/nginx/sites-available/app1.domain.com)

Nginx

server {
    server_name app1.yourdomain.com;

    location / {
        proxy_pass http://localhost:3000; # Points to App 1
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}
  • Free SSL (Let’s Encrypt):Bashsudo apt install certbot python3-certbot-nginx sudo certbot --nginx -d app1.yourdomain.com -d app2.yourdomain.com

3. Automated CI/CD Pipeline (GitHub Actions)

To enable automatic deployments whenever you push code to main, set up GitHub Actions via SSH.

Step 1: Generate Deployment Key on VPS

Create an SSH key specifically for your deployment pipeline:

Bash

ssh-keygen -t ed25519 -C "github-actions-deploy" -f ~/.ssh/github_deploy
cat ~/.ssh/github_deploy.pub >> ~/.ssh/authorized_keys

Copy the contents of the private key (~/.ssh/github_deploy).

Step 2: Add Secrets to GitHub Repository

In your GitHub Repo, go to Settings > Secrets and variables > Actions and add:

  • SSH_PRIVATE_KEY: Content of ~/.ssh/github_deploy
  • VPS_IP: Your VPS Server IP address
  • VPS_USER: Your SSH user (e.g., root or usman)

Step 3: Create Workflow File (.github/workflows/deploy.yml)

YAML

name: Deploy Node Application

on:
  push:
    branches: [ main ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4

      - name: Deploy to VPS via SSH
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.VPS_IP }}
          username: ${{ secrets.VPS_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd /var/www/alphanetix-api
            git pull origin main
            npm ci --production
            npm run build --if-present
            pm2 reload ecosystem.config.js --update-env

Note: pm2 reload performs a zero-downtime deployment by restarting cluster workers one by one rather than stopping the entire service.

4. Key Security & Operational Best Practices

  1. Non-Root Execution: Never run Node.js apps as the root system user. Create a dedicated application user (e.g., deploy or www-data).
  2. Environment File Security: Store sensitive configuration in a .env file directly on the VPS (/var/www/app/.env). Never commit .env files to GitHub.
  3. Firewall Discipline: Keep application internal ports (like 3000, 3001) blocked from external access in ufw. Only expose 80, 443, 22 (SSH), and your database ports (if strictly necessary and protected with strong passwords).
  4. Automate Log Rotation: PM2 logs can fill up disk space quickly over time. Install the log rotate module:Bashpm2 install pm2-logrotate