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.jsfile 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:Bash
pm2 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):Bash
sudo 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_deployVPS_IP: Your VPS Server IP addressVPS_USER: Your SSH user (e.g.,rootorusman)
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 reloadperforms a zero-downtime deployment by restarting cluster workers one by one rather than stopping the entire service.
4. Key Security & Operational Best Practices
- Non-Root Execution: Never run Node.js apps as the
rootsystem user. Create a dedicated application user (e.g.,deployorwww-data). - Environment File Security: Store sensitive configuration in a
.envfile directly on the VPS (/var/www/app/.env). Never commit.envfiles to GitHub. - Firewall Discipline: Keep application internal ports (like 3000, 3001) blocked from external access in
ufw. Only expose80,443,22(SSH), and your database ports (if strictly necessary and protected with strong passwords). - Automate Log Rotation: PM2 logs can fill up disk space quickly over time. Install the log rotate module:Bash
pm2 install pm2-logrotate