GitHub Actions is now the default CI/CD platform for most teams — it is free for public repos, integrates natively with GitHub, and requires no separate infrastructure to manage. This guide gets you from zero to a production-grade pipeline with automated testing, Docker builds, and deployment.
Core Concepts in 60 Seconds
- Workflow: a YAML file in .github/workflows/ — triggered by events
- Event: push, pull_request, schedule, workflow_dispatch (manual), etc.
- Job: a unit of work that runs on a runner — jobs run in parallel by default
- Step: an individual command or action within a job — steps run sequentially
- Runner: the machine that executes your job — ubuntu-latest is free and covers 90% of use cases
- Action: a reusable unit (actions/checkout, actions/setup-node) from the marketplace
Node.js CI Pipeline
yaml
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20, 22] # Test against multiple versions
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: testpassword
POSTGRES_DB: testdb
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci # Use ci not install — faster, uses package-lock.json exactly
- name: Run linter
run: npm run lint
- name: Run type check
run: npm run typecheck
- name: Run tests
run: npm test -- --coverage
env:
DATABASE_URL: postgresql://postgres:testpassword@localhost:5432/testdb
- name: Upload coverage
uses: codecov/codecov-action@v4
if: matrix.node-version == 22 # Only upload once
Docker Build and Push
yaml
# .github/workflows/docker.yml
name: Build and Push Docker Image
on:
push:
branches: [main]
tags: ['v*'] # Also trigger on version tags
jobs:
docker:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write # Required to push to GitHub Container Registry
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=sha,prefix=sha-
type=ref,event=branch
type=semver,pattern={{version}}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha # GitHub Actions cache — dramatically speeds up rebuilds
cache-to: type=gha,mode=max
Deployment to a VPS/Server
yaml
# .github/workflows/deploy.yml
name: Deploy to Production
on:
workflow_run:
workflows: ["Build and Push Docker Image"]
types: [completed]
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.PROD_HOST }}
username: ${{ secrets.PROD_USER }}
key: ${{ secrets.PROD_SSH_KEY }}
script: |
# Pull latest image
docker pull ghcr.io/${{ github.repository }}:main
# Zero-downtime restart using Docker Compose
cd /opt/myapp
docker compose pull
docker compose up -d --remove-orphans
# Verify deployment
sleep 10
curl -f http://localhost:3000/health || exit 1
# Clean up old images
docker image prune -f
Secrets Management
Store secrets in GitHub repository Settings → Secrets and variables → Actions. Never hardcode credentials in workflow files.
- GITHUB_TOKEN is automatically provided — use it for pushing to GHCR and creating releases
- Environment secrets (Settings → Environments) can be restricted to specific branches
- Use environment protection rules to require manual approval before deploying to production
- Rotate secrets quarterly — use the GitHub CLI to update: gh secret set MY_SECRET
- Scan for accidentally committed secrets: add trufflesecurity/trufflehog-actions to your CI
Cost Optimisation
NOTE
GitHub Actions is free for public repositories with unlimited minutes. For private repos: 2,000 minutes/month free on the Free plan, 3,000 on Pro, 50,000 on Teams. Ubuntu runners cost 0.008 USD/minute. Optimise by caching dependencies and using job conditions to skip unnecessary runs.
- Cache node_modules with actions/cache or use actions/setup-node cache option
- Use paths filters to skip CI for docs-only changes: on.push.paths
- Run expensive jobs (E2E tests) only on PRs to main, not every feature branch push
- Use concurrency to cancel in-progress runs when a new push arrives on the same branch
- Self-hosted runners for long-running jobs — 10× cheaper than GitHub-hosted at scale
Category:DevOps