Build a production-ready CI/CD workflow for Next.js applications with GitHub Actions, automated testing, and zero-downtime deployments
Manually deploying your Next.js application every time you push code is tedious, error-prone, and slows down your development velocity. A proper CI/CD (Continuous Integration/Continuous Deployment) pipeline automates testing, building, and deploying your application, allowing you to ship features faster with confidence.
In this comprehensive guide, you'll learn how to implement a complete CI/CD pipeline for Next.js applications using GitHub Actions, including automated testing, linting, building, and deployment to popular platforms.
Why CI/CD Matters for Next.js Applications
Modern Next.js applications are complex. They use TypeScript, require build optimization, leverage server-side rendering, and often integrate with multiple services. A robust CI/CD pipeline ensures:
- Consistent builds across all environments
- Automated testing catches bugs before production
- Faster deployment cycles with zero manual intervention
- Rollback capabilities when issues arise
- Team collaboration without deployment bottlenecks
Let's build one from scratch.
Prerequisites
Before implementing CI/CD, ensure you have:
- A Next.js application in a Git repository
- A GitHub account (we'll use GitHub Actions)
- A deployment target (Vercel, AWS, DigitalOcean, etc.)
- Basic understanding of YAML syntax
Setting Up the Foundation
Project Structure Best Practices
First, organize your Next.js project for CI/CD success:
nextjs-app/
├── .github/
│ └── workflows/
│ ├── ci.yml
│ └── deploy.yml
├── src/
├── tests/
├── package.json
├── next.config.js
└── .env.example
Environment Variables Management
Create a .env.example file documenting all required environment variables:
# .env.example
NEXT_PUBLIC_API_URL=
DATABASE_URL=
AUTH_SECRET=
STRIPE_SECRET_KEY=
Never commit actual .env files. Store secrets in GitHub Secrets or your deployment platform's secret management.
Building the CI Pipeline
Create .github/workflows/ci.yml for continuous integration:
name: CI Pipeline
on:
pull_request:
branches: [main, develop]
push:
branches: [main, develop]
jobs:
lint-and-test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x, 20.x]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run ESLint
run: npm run lint
- name: Run TypeScript check
run: npm run type-check
- name: Run unit tests
run: npm run test
- name: Run build
run: npm run build
env:
NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }}
Adding Test Scripts to package.json
Update your package.json with necessary scripts:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"type-check": "tsc --noEmit",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}
Implementing Automated Testing
Setting Up Jest for Next.js
Install testing dependencies:
npm install -D jest @testing-library/react @testing-library/jest-dom jest-environment-jsdom
Create jest.config.js:
const nextJest = require('next/jest')
const createJestConfig = nextJest({
dir: './',
})
const customJestConfig = {
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
testEnvironment: 'jest-environment-jsdom',
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/**/*.d.ts',
'!src/**/*.stories.{js,jsx,ts,tsx}',
],
}
module.exports = createJestConfig(customJestConfig)
Writing Component Tests
Example test for a Next.js component:
// __tests__/components/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react'
import Button from '@/components/Button'
describe('Button Component', () => {
it('renders button with correct text', () => {
render(<Button>Click me</Button>)
expect(screen.getByText('Click me')).toBeInTheDocument()
})
it('calls onClick handler when clicked', () => {
const handleClick = jest.fn()
render(<Button onClick={handleClick}>Click me</Button>)
fireEvent.click(screen.getByText('Click me'))
expect(handleClick).toHaveBeenCalledTimes(1)
})
})
Creating the CD Pipeline
Deploying to Vercel
Create .github/workflows/deploy-vercel.yml:
name: Deploy to Vercel
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Deploy to Vercel
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
vercel-args: '--prod'
Deploying to AWS or DigitalOcean
For custom server deployments, create .github/workflows/deploy-production.yml:
name: Deploy to Production
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build application
run: npm run build
env:
NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }}
- name: Deploy to server
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /var/www/nextjs-app
git pull origin main
npm ci
npm run build
pm2 reload nextjs-app
Advanced CI/CD Features
Parallel Testing for Faster Builds
Speed up your CI pipeline by running tests in parallel:
test:
runs-on: ubuntu-latest
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- name: Run tests
run: npm run test -- --shard=${{ matrix.shard }}/4
Automated Lighthouse Performance Checks
Add performance testing to your pipeline:
- name: Run Lighthouse CI
run: |
npm install -g @lhci/cli
lhci autorun
env:
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
Automatic Dependency Updates
Use Dependabot to keep dependencies updated. Create .github/dependabot.yml:
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
Preview Deployments for Pull Requests
Deploy every PR to a preview environment:
name: Preview Deployment
on:
pull_request:
types: [opened, synchronize]
jobs:
preview:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Deploy Preview
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
Security Best Practices
Protecting Secrets
Store all sensitive data in GitHub Secrets:
- Go to repository Settings → Secrets and variables → Actions
- Add secrets like
VERCEL_TOKEN,DATABASE_URL, etc. - Reference them in workflows using
${{ secrets.SECRET_NAME }}
Dependency Scanning
Add automated security scanning:
- name: Run security audit
run: npm audit --audit-level=high
- name: Check for vulnerabilities
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
Monitoring and Notifications
Slack Notifications for Deployments
Add deployment notifications:
- name: Notify Slack
uses: 8398a7/action-slack@v3
if: always()
with:
status: ${{ job.status }}
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
Discord Notifications
- name: Discord notification
uses: Ilshidur/action-discord@master
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
with:
args: 'Deployment to production completed!'
Troubleshooting Common Issues
Build Failures
If builds fail in CI but work locally, check:
- Node version consistency
- Environment variables are properly set
- Dependencies are locked in
package-lock.json
Deployment Timeouts
For large Next.js apps, increase timeout limits:
- name: Build application
run: npm run build
timeout-minutes: 20
The Complete Workflow
Here's what your production-ready CI/CD pipeline accomplishes:
- On every commit: Lint, type-check, and test
- On pull requests: Run full CI suite + deploy preview
- On merge to main: Build, test, and deploy to production
- Continuously: Scan for security vulnerabilities
- Weekly: Update dependencies automatically
Conclusion
Implementing CI/CD for your Next.js application transforms your development workflow. You ship faster, catch bugs earlier, and deploy with confidence. Start with the basic CI pipeline, add automated testing, then progressively enhance with preview deployments, performance checks, and security scanning.
The investment in setting up CI/CD pays dividends immediately. Your team focuses on building features while automation handles the repetitive, error-prone deployment tasks.
Ready to implement CI/CD? Start with the workflows in this guide, customize them for your needs, and watch your deployment velocity skyrocket.
What CI/CD challenges have you faced with Next.js? Share your experiences and solutions in the comments below.
Top comments (0)