DEV Community

Cover image for Automate Deploying Your Node.js App to a VPS with GitHub Actions & Docker Compose
hacker_ea
hacker_ea

Posted on

Automate Deploying Your Node.js App to a VPS with GitHub Actions & Docker Compose

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

  1. Generate an SSH key pair, store the private key in your GitHub repo’s Secrets.
  2. Create a GitHub Actions workflow that, on every push to main, SSHes into your VPS and runs docker-compose pull && docker-compose up -d.
  3. 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 Dockerfile that builds your app.
    • A docker-compose.yml defining at minimum your web service (and any dependencies).

2. Set Up SSH Authentication

  1. On your local machine, generate a key without passphrase:
   ssh-keygen -t rsa -b 4096 -C "deploy@your-domain" -f ~/.ssh/id_rsa_vps
Enter fullscreen mode Exit fullscreen mode
  1. Copy the public key to your VPS:
   ssh-copy-id -i ~/.ssh/id_rsa_vps.pub root@your-vps-ip
Enter fullscreen mode Exit fullscreen mode
  1. In GitHub, add a new Secret named SSH_PRIVATE_KEY, and paste in the contents of ~/.ssh/id_rsa_vps.
  2. (Optional) Add a Secret SSH_KNOWN_HOSTS containing the output of:
   ssh-keyscan -H your-vps-ip
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Key benefits

  • Security: Private key never leaves GitHub Actions.
  • Atomic switch: docker-compose pull fetches the specific tagged image, then up -d swaps containers instantly.
  • Rollback: Redeploy a prior commit by resetting main to 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.sh for clarity and reuse.
  • Zero-downtime: Consider rolling updates with Docker Compose v2’s deploy options 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)