All posts
DevOpsJun 2, 2026·7 min read

CI/CD Pipeline for Node.js Using GitHub Actions

A complete GitHub Actions workflow: install, lint, test, build Docker image, push to registry, deploy. Includes secrets management, matrix testing, and caching for fast runs.

The complete workflow

Here's a production-ready GitHub Actions workflow that handles the full CI/CD pipeline for a Node.js app.

name: CI/CD

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [20, 22]

    steps:
      - uses: actions/checkout@v4

      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'

      - run: npm ci
      - run: npm run lint
      - run: npm run test:coverage

      - name: Upload coverage
        uses: codecov/codecov-action@v4

  deploy:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'

    steps:
      - uses: actions/checkout@v4

      - name: Build and push Docker image
        uses: docker/build-push-action@v5
        with:
          push: true
          tags: registry.example.com/myapp:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Deploy to production
        run: |
          curl -X POST ${{ secrets.DEPLOY_WEBHOOK_URL }}             -H "Authorization: Bearer ${{ secrets.DEPLOY_TOKEN }}"             -d '{"image": "registry.example.com/myapp:${{ github.sha }}"}'

Key elements explained

Matrix testing — Run tests against Node 20 and 22 simultaneously. Catch compatibility issues before they reach production.

cache: 'npm' — Caches the npm module cache between runs. Cuts install time from ~60s to ~5s on cache hit.

docker/build-push-action with GHA cache — Docker layer caching via GitHub Actions cache. Unchanged layers are reused. A change to src/ only rebuilds from the COPY layer, not from npm install.

needs: test — The deploy job only runs if the test job passes. Never deploy without green tests.

if: github.ref == 'refs/heads/main' — Only deploy on pushes to main. PRs run tests but don't deploy.

Managing secrets

Store all sensitive values in GitHub repository secrets (Settings → Secrets → Actions). Reference them with ${{ secrets.MY_SECRET }}. Never hardcode credentials in workflows.

For environment-specific secrets (staging vs production), use GitHub Environments with separate secret sets and required reviewer approvals for production.

Preview deployments for PRs

  preview:
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    steps:
      - name: Deploy preview
        id: deploy
        run: |
          URL=$(deploy-preview --branch ${{ github.head_ref }})
          echo "url=$URL" >> $GITHUB_OUTPUT

      - name: Comment PR
        uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: '🚀 Preview deployed to ${{ steps.deploy.outputs.url }}'
            })

Workflow runtime targets

A well-optimized CI pipeline should complete in under 5 minutes. If yours takes longer, investigate: npm install without caching, running tests serially instead of parallel, or building Docker images without layer caching are the usual culprits.

Ready to put this into practice?

Deploy your Node.js app to production in minutes — zero YAML, automatic CI/CD, and HTTPS included.