Setting Up CI/CD for a Node.js Application on a VPS

This guide provides a end-to-end reference for deploying Node.js applications to a Linux Virtual Private Server (VPS) automatically using GitHub Actions, PM2, and Nginx.

🏗️ Architecture Overview

  • Hosting Platform: Ubuntu VPS (Contabo or similar)
  • Web Server / Reverse Proxy: Nginx (routes subdomains to application ports, e.g., shop.usmanlive.com $\rightarrow$ 127.0.0.1:5000)
  • Process Management: PM2 (keeps Node.js running in the background and enables zero-downtime reloads)
  • CI/CD Pipeline: GitHub Actions (appleboy/ssh-action) triggered on pushes to the main branch

Step 1: VPS Initial Configuration

1. Install Node.js & PM2

Connect to your VPS via SSH and install the required Node.js runtime and PM2 globally:

Bash

# Update local packages
sudo apt update && sudo apt upgrade -y

# Install Node.js v24 (or your target LTS version) via NodeSource
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
sudo apt install -y nodejs

# Install PM2 globally
sudo npm install -g pm2

2. Set Up Application Directory & Permissions

Create a dedicated application directory and ensure your SSH deployment user owns it:

Bash

# Create directory structure
sudo mkdir -p /var/www/apps/node-shop

# Transfer ownership to your SSH user (e.g., root or deploy user)
sudo chown -R $USER:$USER /var/www/apps/node-shop

3. Clone Repository & Run Initial App Instance

Clone the repository manually once to install production dependencies and start the initial PM2 process:

Bash

cd /var/www/apps/node-shop
git clone https://github.com/your-username/your-repo.git .

# Navigate to the app folder (if nested)
cd web-app
npm install --production

# Start app with PM2 and save state
pm2 start index.js --name "node-shop"
pm2 save

Step 2: Nginx Reverse Proxy Setup

Configure Nginx to proxy incoming web traffic to the local port running your Node app (e.g., port 5000):

  1. Create a configuration file under /etc/nginx/sites-available/node-shop:

Nginx

server {
    listen 80;
    server_name shop.usmanlive.com;

    location / {
        proxy_pass http://127.0.0.1:5000;
        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;
    }
}
  1. Enable the site configuration and restart Nginx:

Bash

sudo ln -s /etc/nginx/sites-available/node-shop /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Step 3: Configure GitHub Secrets

To allow GitHub Actions to SSH into your server securely without hardcoding credentials:

  1. In GitHub, go to: Settings $\rightarrow$ Secrets and variables $\rightarrow$ Actions $\rightarrow$ New repository secret.
  2. Add the following Repository Secrets:
Secret NameValue ExampleDescription
VPS_HOST169.58.107.175Your VPS public IPv4 or IPv6 address
VPS_USERNAMErootYour SSH deployment username
VPS_SSH_KEY-----BEGIN OPENSSH...Complete private SSH key content

Step 4: Add GitHub Actions Deployment Workflow

Create a workflow file at .github/workflows/deploy.yml in your repository:

YAML

name: Deploy Node.js Web App

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      # Step 1: Checkout the latest code from GitHub
      - name: Checkout Repository
        uses: actions/checkout@v5

      # Step 2: SSH into VPS and pull code + restart app
      - name: Deploy to VPS via SSH
        uses: appleboy/ssh-action@v1.2.0
        with:
          host: ${{ secrets.VPS_HOST }}
          username: ${{ secrets.VPS_USERNAME }}
          key: ${{ secrets.VPS_SSH_KEY }}
          script: |
            cd /var/www/apps/node-shop
            git fetch origin main
            git reset --hard origin/main
            cd web-app
            npm install --production
            pm2 reload node-shop || pm2 start index.js --name "node-shop"

Step 5: Verification & Deployment Workflow

  1. Push changes to the main branch.
  2. Go to the Actions tab on GitHub to observe execution.
  3. Once the workflow completes:
    • GitHub Actions executes git fetch and git reset on the VPS.
    • npm install --production updates runtime dependencies.
    • pm2 reload node-shop performs a zero-downtime restart of your Node.js application.