Automate Deploying Your Node.js App to a VPS with GitHub Actions & Docker Compose
A step-by-step guide to a simple, secure, and reproducible CI/CD pipeline.
TL;DR
- Generate an SSH key pair, store the private key in your GitHub repo’s Secrets.
 - 
Create a GitHub Actions workflow that, on every push to 
main, SSHes into your VPS and runsdocker-compose pull && docker-compose up -d. - 
Structure your VPS with one project folder per app, each containing its own 
docker-compose.yml. 
Why This Matters
Manually deploying via SSH and git pull on a VPS (DigitalOcean, OVH, Scaleway, etc.) works at first—but as your team and release cadence grow, manual steps lead to missed updates and unexpected downtime. Pairing GitHub Actions with Docker Compose gives you:
- Atomic deployments: Docker images are versioned and immutable.
 - Instant rollbacks: Revert to a previous commit in seconds.
 - Clear visibility: Build and deploy logs accessible in GitHub’s UI.
 
1. Prerequisites
- A Linux VPS (Ubuntu 20.04+ recommended) with Docker & Docker Compose installed.
 - 
A GitHub repo containing:
- Your Node.js source code (
package.json, etc.). - A 
Dockerfilethat builds your app. - A 
docker-compose.ymldefining at minimum yourwebservice (and any dependencies). 
 - Your Node.js source code (
 
2. Set Up SSH Authentication
- On your local machine, generate a key without passphrase:
 
   ssh-keygen -t rsa -b 4096 -C "deploy@your-domain" -f ~/.ssh/id_rsa_vps
- Copy the public key to your VPS:
 
   ssh-copy-id -i ~/.ssh/id_rsa_vps.pub root@your-vps-ip
- 
In GitHub, add a new Secret named 
SSH_PRIVATE_KEY, and paste in the contents of~/.ssh/id_rsa_vps. - 
(Optional) Add a Secret 
SSH_KNOWN_HOSTScontaining the output of: 
   ssh-keyscan -H your-vps-ip
This pins your VPS’s fingerprint.
3. Sample Dockerfile
# Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
CMD ["node", "server.js"]
4. Sample docker-compose.yml
version: "3.8"
services:
  web:
    image: your-dockerhub-user/your-app:${GITHUB_SHA::8}
    build:
      context: .
    ports:
      - "3000:3000"
    restart: always
We tag images with the first 8 characters of the Git commit SHA to trace exactly what’s running.
5. GitHub Actions Workflow
Create .github/workflows/deploy.yml in your repo:
name: CI/CD to VPS
on:
  push:
    branches: [ main ]
jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
      - name: Set up SSH
        uses: webfactory/ssh-agent@v0.8.1
        with:
          ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
      - name: (Optional) Add known_hosts
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.SSH_KNOWN_HOSTS }}" >> ~/.ssh/known_hosts
      - name: Build Docker image
        run: |
          docker build \
            --tag your-dockerhub-user/your-app:${GITHUB_SHA::8} \
            .
      - name: Log in to Docker Hub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKERHUB_USER }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      - name: Push image
        run: |
          docker push your-dockerhub-user/your-app:${GITHUB_SHA::8}
      - name: Deploy to VPS
        run: |
          ssh root@your-vps-ip << 'EOF'
            cd /srv/your-app
            export GITHUB_SHA=${GITHUB_SHA}
            docker-compose pull
            docker-compose up -d
          EOF
Key benefits
- Security: Private key never leaves GitHub Actions.
 - 
Atomic switch: 
docker-compose pullfetches the specific tagged image, thenup -dswaps containers instantly. - 
Rollback: Redeploy a prior commit by resetting 
mainto an older SHA. 
6. Best Practices & Next Steps
- 
Build cache: Accelerate builds with 
actions/cache@v3+ BuildKit. - 
Matrix builds: Test against multiple Node.js versions with a 
strategy.matrix. - Alerts: Add a Slack or Microsoft Teams step to notify on failures.
 - 
Reusable scripts: Encapsulate deploy logic in 
scripts/deploy.shfor clarity and reuse. - 
Zero-downtime: Consider rolling updates with Docker Compose v2’s 
deployoptions or switch to Docker Swarm/Kubernetes as you scale. 
Conclusion
With about 15 lines of YAML and a pair of SSH keys, you’ll have a robust, transparent CI/CD pipeline for any VPS-hosted project and improve your web app. You’ll gain reliability, speed, and traceability—and deployments will finally be… automatic!
Give it a try: adapt this approach to Python, Go, or PHP stacks, add automated tests or security scans, and share your experiences in the comments.
              
    
Top comments (0)