CI/CD Pipelines
Continuous Integration / Continuous Deployment automates the path from a code change to a running deployment. CI runs your checks (tests, lint, type-check, security audit) on every push or pull request to catch problems fast and block bad code from merging. CD takes every successful build on the main branch and deploys it, so the gap between "merged" and "in production" is minutes rather than days. Together they make deployments low-risk, frequent, and boring — which is exactly what you want. This page covers a GitHub Actions workflow for a Node app: the CI check pipeline, building and pushing a Docker image, deploying, and the patterns that make pipelines reliable.
A CI pipeline for Node
.github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' # caches node_modules between runs → faster
- run: npm ci # exact install, lock file enforced
- run: npm run lint # ESLint
- run: npm run typecheck # tsc --noEmit
- run: npm test # unit + integration tests
- run: npm audit --audit-level=high # fail on high/critical vulnerabilitiesBuilding and pushing a Docker image
.github/workflows/deploy.yml (deploy job)
build-and-push:
needs: check # only runs if CI checks pass
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' # only on main branch
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:
push: true
tags: ghcr.io/${{ github.repository }}:latest,ghcr.io/${{ github.repository }}:${{ github.sha }}
cache-from: type=gha # layer cache between workflow runs
cache-to: type=gha,mode=maxDeployment step
deploy:
needs: build-and-push
runs-on: ubuntu-latest
steps:
# Pattern varies by platform — examples:
# Render / Railway — trigger a deploy via API:
- run: curl -X POST ${{ secrets.DEPLOY_HOOK_URL }}
# SSH to a server and pull the new image (for VMs / bare metal):
- uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SERVER_HOST }}
username: deploy
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
docker pull ghcr.io/${{ github.repository }}:${{ github.sha }}
docker stop api && docker rm api
docker run -d --name api -p 3000:3000 \
-e NODE_ENV=production \
-e DATABASE_URL=${{ secrets.DATABASE_URL }} \
ghcr.io/${{ github.repository }}:${{ github.sha }}Pipeline best practices
Protect the main branch — require CI checks to pass and at least one review before merging; make CI the gatekeeper.
Build once, deploy the artifact — one image build per commit; the same image goes to staging and production.
Keep secrets in CI secrets — never put credentials in workflow files; use encrypted secrets and pass them as environment variables.
Cache aggressively —
node_modulesviaactions/setup-node cache, Docker layers viatype=gha, and any other repeatable work.Run in parallel where independent — lint, type-check, and test can run as parallel jobs; docker build can start as soon as checkout finishes.
Fail fast — put cheap, fast checks (lint) before expensive ones (e2e tests); use
needs:to skip later stages if early ones fail.