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
Dockerfile
that builds your app. - A
docker-compose.yml
defining at minimum yourweb
service (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_HOSTS
containing 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 pull
fetches the specific tagged image, thenup -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)