Stop SSH-ing into your server and running git pull manually. Set up CI/CD and every push to main deploys automatically.
The Simple Pipeline
Push to GitHub -> GitHub Actions builds -> Deploy to VPS
Method 1: GitHub Actions + SSH
GitHub Actions Workflow
# .github/workflows/deploy.yml
name: Deploy to VPS
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /var/www/myapp
git pull origin main
npm install --production
npm run build
pm2 restart myapp
Setting Up Secrets
In your GitHub repository settings:
VPS_HOST: Your VPS IP addressVPS_USER: SSH usernameSSH_PRIVATE_KEY: Your private SSH key
Method 2: Docker-Based Deployment
Build Docker Image in CI
# .github/workflows/deploy.yml
name: Build and Deploy
on:
push:
branches: [main]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Docker image
run: docker build -t myapp:latest .
- name: Save and transfer image
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /opt/myapp
docker compose pull
docker compose up -d --build
Docker Compose on VPS
# docker-compose.yml
services:
app:
build: .
ports:
- "3000:3000"
restart: always
environment:
- NODE_ENV=production
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
depends_on:
- app
restart: always
Method 3: Webhook-Based Deploy
Simple Deploy Script
#!/bin/bash
# /opt/deploy/deploy.sh
cd /var/www/myapp
git pull origin main
npm install --production
npm run build
pm2 restart myapp
echo "Deployed at $(date)" >> /var/log/deploy.log
Webhook Listener
// Simple webhook server (Node.js)
const http = require('http');
const { execSync } = require('child_process');
const crypto = require('crypto');
const SECRET = process.env.WEBHOOK_SECRET;
http.createServer((req, res) => {
if (req.method === 'POST' && req.url === '/deploy') {
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => {
const sig = req.headers['x-hub-signature-256'];
const expected = 'sha256=' + crypto
.createHmac('sha256', SECRET)
.update(body)
.digest('hex');
if (crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
execSync('/opt/deploy/deploy.sh');
res.writeHead(200);
res.end('Deployed');
} else {
res.writeHead(403);
res.end('Unauthorized');
}
});
}
}).listen(9000);
Zero-Downtime Deployment
Blue-Green with PM2
# Deploy new version alongside old
pm2 start ecosystem.config.js --env production
# Wait for new version to be ready
sleep 5
# Switch traffic to new version
pm2 reload myapp
# Old version gracefully shuts down
With Nginx Upstream
upstream myapp {
server 127.0.0.1:3000;
server 127.0.0.1:3001 backup;
}
Deploy to the backup port, test it, then swap.
Testing Before Deployment
Add tests to your pipeline:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm install
- run: npm test
deploy:
needs: test # Only deploy if tests pass
runs-on: ubuntu-latest
steps:
- name: Deploy via SSH
# ... deploy steps
Deployment Checklist
| Step | Automated? | How | |------|-----------|-----| | Run tests | Yes | GitHub Actions | | Build application | Yes | GitHub Actions | | Transfer to VPS | Yes | SSH/Docker | | Install dependencies | Yes | npm install in deploy script | | Restart application | Yes | PM2/Docker | | Health check | Should be | Curl endpoint after deploy | | Rollback if failed | Semi | Keep previous version available |
Every push to main triggers a fully automated deployment on your Space-Node VPS. No manual intervention needed.
