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.
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
# .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:
{
"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:
- 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:
// 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:
- 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
- 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:
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
- 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:
- 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:
# 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:
| Environment | Variables source |
|---|---|
| Local dev | .env.local (gitignored) |
| CI/Test | GitHub Actions secrets |
| Preview | Vercel environment variables (preview) |
| Production | Vercel 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:
- 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:
- 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
// .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:
- 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:
- TypeScript check — catches type errors
- ESLint — catches code quality issues
- Unit tests — catches logic regressions
- Build — catches build-time errors
- 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.