GitHub Actions is a powerful CI/CD platform that starts with a generous free tier and is unlimited for public repositories. Run tests on every push, build containers, deploy to production — all in a single YAML file. This guide walks through a complete CI/CD pipeline for a Node.js project from scratch.
Basic Workflow Structure
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm test
Matrix Strategy
Run the same job in parallel across multiple Node versions, operating systems or environments.
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node: [18, 20, 22]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm ci && npm test
Docker Image Build + Registry Push
jobs:
docker:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
ghcr.io/${{ github.repository }}:latest
ghcr.io/${{ github.repository }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
Secret Management
Define secrets under Settings → Secrets and variables → Actions. They are automatically masked in logs. For environment-specific secrets, use environments (manual approval can be added for promotion to production).
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- run: echo ${{ secrets.DATABASE_URL }}
- uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /var/www/app
git pull
npm ci --production
pm2 reload ecosystem.config.js
Service Containers (Testing with a DB)
jobs:
integration-test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: test
ports: ['5432:5432']
options: --health-cmd pg_isready --health-interval 5s --health-timeout 5s --health-retries 5
redis:
image: redis:7
ports: ['6379:6379']
env:
DATABASE_URL: postgres://postgres:test@localhost:5432/postgres
REDIS_URL: redis://localhost:6379
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- run: npm run migrate
- run: npm run test:integration
Cache Optimization
- uses: actions/cache@v4
with:
path: |
~/.npm
node_modules
.next/cache
key: ${{ runner.os }}-build-${{ hashFiles('**/package-lock.json') }}
restore-keys: ${{ runner.os }}-build-
Reusable Workflows
# .github/workflows/reusable-deploy.yml
name: Deploy
on:
workflow_call:
inputs:
env: { required: true, type: string }
secrets:
SSH_KEY: { required: true }
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- run: echo Deploying to ${{ inputs.env }}
# Call it from the main workflow
jobs:
prod:
uses: ./.github/workflows/reusable-deploy.yml
with: { env: production }
secrets: inherit
Workflow Dispatch (Manual Trigger)
on:
workflow_dispatch:
inputs:
environment:
type: choice
options: [staging, production]
version:
type: string
default: latest
Self-Hosted Runners
The free GitHub-hosted runners have limits (2,000 minutes/month for public repos, more for paid plans). For heavier jobs, run your own self-hosted runner — Settings → Actions → Runners → New self-hosted runner.
Common Pitfalls
GITHUB_TOKENpermissions are restricted by default — addpermissions:to the workflow when needed- Concurrent runs can clash when deploying to the same environment — control with
concurrency: actions/checkout@v4fetches only one commit by default — usefetch-depth: 0for full history- Use
npm ciinstead ofnpm installfor dependency installs — it honors the lockfile exactly
Conclusion
With GitHub Actions you get production-grade CI/CD from 50 lines of YAML. You can stand up a full test + build + deploy + notification chain in one afternoon. This is the golden age of being able to deploy on every push with confidence.
Reach out for GitHub Actions, GitLab CI or Jenkins pipeline design and implementation. Contact us