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 themainbranch
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):
- 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;
}
}
- 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:
- In GitHub, go to: Settings $\rightarrow$ Secrets and variables $\rightarrow$ Actions $\rightarrow$ New repository secret.
- Add the following Repository Secrets:
| Secret Name | Value Example | Description |
VPS_HOST | 169.58.107.175 | Your VPS public IPv4 or IPv6 address |
VPS_USERNAME | root | Your 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
- Push changes to the
mainbranch. - Go to the Actions tab on GitHub to observe execution.
- Once the workflow completes:
- GitHub Actions executes
git fetchandgit reseton the VPS. npm install --productionupdates runtime dependencies.pm2 reload node-shopperforms a zero-downtime restart of your Node.js application.
- GitHub Actions executes