All guides
Engineering11 min read

CI/CD Pipeline Setup for Next.js Projects

A complete guide to building a production-ready CI/CD pipeline for Next.js — from automated testing and type checking to preview deployments and production rollouts.

NC

Nextcraft Agency

The CI/CD Philosophy

A CI/CD pipeline is a system that catches problems before they reach production. The investment — setting it up, maintaining it, waiting for it on every PR — pays back in prevented incidents, faster debugging, and the confidence to ship quickly.

The goal: merge a PR and know with high confidence that it won't break production. Any confidence gap should prompt you to add a test or check to the pipeline.


Part 1: The GitHub Actions Foundation

GitHub Actions is the most practical choice for Next.js CI/CD. It integrates with GitHub's PR review workflow, has excellent documentation, and the free tier covers most teams' needs.

The Basic Pipeline Structure

code
# .github/workflows/ci.yml
name: CI

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

jobs:
  ci:
    name: Type Check, Lint, Test, Build
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Type check
        run: npm run typecheck
      
      - name: Lint
        run: npm run lint
      
      - name: Unit tests
        run: npm test -- --coverage
      
      - name: Build
        run: npm run build
        env:
          # Required env vars for build
          NEXT_PUBLIC_APP_URL: ${{ vars.APP_URL }}

Add the required scripts to package.json:

code
{
  "scripts": {
    "typecheck": "tsc --noEmit",
    "lint": "next lint",
    "test": "jest",
    "build": "next build"
  }
}

Caching for Speed

Build times are dominated by npm ci and Next.js build. Cache both:

code
- uses: actions/setup-node@v4
  with:
    node-version: '20'
    cache: 'npm'  # Caches node_modules

- name: Cache Next.js build
  uses: actions/cache@v4
  with:
    path: |
      .next/cache
    key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}
    restore-keys: |
      ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-

With both caches warm, a pipeline that takes 4 minutes on first run takes 60–90 seconds on subsequent runs.


Part 2: Type Checking

TypeScript errors caught in CI prevent runtime errors in production. Configure strict type checking:

code
// tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true
  }
}

tsc --noEmit runs the compiler without producing output — faster than a build, catches all type errors.

Path to Clean Type Checking

If your codebase has existing type errors, introduce the check incrementally:

code
- name: Type check (no bail)
  run: npx tsc --noEmit --incremental
  continue-on-error: true  # Warn but don't fail until errors are fixed

Once the codebase is clean, remove continue-on-error. Maintain the zero-error policy from there.


Part 3: Testing in CI

Running Tests with Coverage

code
- name: Unit and integration tests
  run: npm test -- --coverage --ci --runInBand
  env:
    DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}

--ci disables interactive mode and makes tests fail if a snapshot is outdated (instead of prompting to update). --runInBand runs tests serially — slower but avoids flaky test failures from resource contention.

Test Database Setup

Integration tests that touch the database need an isolated database:

code
services:
  postgres:
    image: postgres:16
    env:
      POSTGRES_USER: test
      POSTGRES_PASSWORD: test
      POSTGRES_DB: testdb
    ports:
      - 5432:5432
    options: >-
      --health-cmd pg_isready
      --health-interval 10s
      --health-timeout 5s
      --health-retries 5

steps:
  - name: Run database migrations
    run: npx prisma migrate deploy
    env:
      DATABASE_URL: postgresql://test:test@localhost:5432/testdb
  
  - name: Run tests
    run: npm test
    env:
      DATABASE_URL: postgresql://test:test@localhost:5432/testdb

E2E Tests with Playwright

code
- name: Install Playwright browsers
  run: npx playwright install --with-deps chromium
  
- name: Run E2E tests
  run: npx playwright test
  env:
    BASE_URL: http://localhost:3000

- name: Upload Playwright report
  uses: actions/upload-artifact@v4
  if: failure()  # Only upload on failure to save storage
  with:
    name: playwright-report
    path: playwright-report/

Run E2E tests against a built production build (npm run build && npm run start) rather than the dev server — catches issues that only appear in production mode.


Part 4: Preview Deployments

Preview deployments let every PR get a live, shareable URL with the changes deployed. This is invaluable for designer/developer collaboration and product review.

Vercel Preview Deployments

Vercel creates preview deployments automatically for every push when connected to GitHub. No configuration needed.

For projects not on Vercel, use vercel-action:

code
- name: Deploy preview
  uses: amondnet/vercel-action@v25
  id: deploy
  with:
    vercel-token: ${{ secrets.VERCEL_TOKEN }}
    vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
    vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}

- name: Comment PR with preview URL
  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: ${{ steps.deploy.outputs.preview-url }}`
      })

Part 5: Production Deployment

Deployment Strategy

For most Next.js applications on Vercel: merge to main triggers automatic production deployment. The CI pipeline runs first; deployment only proceeds on success.

For more control:

code
# Separate deployment job that runs after CI passes
deploy:
  name: Deploy to Production
  needs: [ci]  # Only runs if CI job passes
  if: github.ref == 'refs/heads/main'
  runs-on: ubuntu-latest
  
  steps:
    - uses: actions/checkout@v4
    
    - name: Deploy to Vercel
      run: npx vercel deploy --prod --token ${{ secrets.VERCEL_TOKEN }}

Environment Variable Management

Separate environments with separate variable sets:

EnvironmentVariables source
Local dev.env.local (gitignored)
CI/TestGitHub Actions secrets
PreviewVercel environment variables (preview)
ProductionVercel environment variables (production)

Never commit secrets. Use GitHub Secrets for CI and Vercel's environment variable system for deployments.

Database Migrations in CI/CD

Run migrations as part of the deployment pipeline, before starting the new server:

code
- name: Run database migrations
  run: npx prisma migrate deploy
  env:
    DATABASE_URL: ${{ secrets.PRODUCTION_DATABASE_URL }}

- name: Deploy application
  run: npx vercel deploy --prod

The migrate deploy command (not migrate dev) applies pending migrations safely in production.


Part 6: Performance Checks

Lighthouse CI

Prevent performance regressions from shipping:

code
- name: Build for Lighthouse CI
  run: npm run build
  
- name: Run Lighthouse CI
  uses: treosh/lighthouse-ci-action@v11
  with:
    configPath: '.lighthouserc.json'
    uploadArtifacts: true
    temporaryPublicStorage: true
code
// .lighthouserc.json
{
  "ci": {
    "assert": {
      "assertions": {
        "largest-contentful-paint": ["error", { "maxNumericValue": 3000 }],
        "cumulative-layout-shift": ["error", { "maxNumericValue": 0.15 }],
        "total-blocking-time": ["warn", { "maxNumericValue": 300 }]
      }
    }
  }
}

Bundle Size Tracking

Prevent bundle size increases:

code
- name: Check bundle size
  uses: preactjs/compressed-size-action@v2
  with:
    repo-token: ${{ secrets.GITHUB_TOKEN }}
    pattern: '.next/static/**/*.js'
    compression: gzip

This comments on PRs with a diff of bundle size changes — immediately visible to reviewers.


Part 7: Branch Protection Rules

Configure GitHub Branch Protection on main:

  • Require a pull request before merging: No direct pushes to main
  • Require status checks to pass: CI must be green
  • Require branches to be up to date: PRs must be rebased/merged with main before merge
  • Require review from Code Owners: At least one reviewer required

This configuration, combined with a CI pipeline, means: nothing ships to production that hasn't been reviewed and tested.


The Minimum Viable Pipeline

For a new project, start with:

  1. TypeScript check — catches type errors
  2. ESLint — catches code quality issues
  3. Unit tests — catches logic regressions
  4. Build — catches build-time errors
  5. Preview deployment — enables visual review

Add as you grow:

  • E2E tests when critical flows need regression coverage
  • Performance budgets when Core Web Vitals are a priority
  • Database migration checks when schema changes are frequent

A CI/CD pipeline is an investment. Build it incrementally, but build it early — retrofitting it into a mature codebase is harder than growing it alongside the product.

Stay Informed.

Join 1,200+ founders and engineers receiving our monthly deep dives on product engineering, design, and growth.

Insights once a month. No spam. Unsubscribe anytime.