CI/CD Tutorial with GitHub Actions for Next.js
ID | EN

CI/CD Tutorial with GitHub Actions for Next.js

Sunday, Dec 28, 2025

CI/CD (Continuous Integration/Continuous Deployment) is a DevOps practice that enables developer teams to automate testing and deployment processes. With GitHub Actions, you can set up a powerful pipeline directly from your GitHub repository without needing external tools.

What is CI/CD?

Continuous Integration (CI) is a practice where every code change is merged to the main branch regularly. Each merge triggers an automated build and test to detect bugs early.

Continuous Deployment (CD) is an extension of CI where every change that passes testing is automatically deployed to production.

Benefits of CI/CD

  • Faster bug detection
  • Reduced manual work
  • More consistent deployments
  • Faster feedback loop
  • Increased confidence when releasing

GitHub Actions Basics

GitHub Actions is a CI/CD platform integrated directly with GitHub. You define workflows in YAML files in the .github/workflows/ folder.

Basic Concepts

  • Workflow: Automated process you define in the repository
  • Event: Trigger that starts the workflow (push, pull_request, schedule, etc)
  • Job: A set of steps that run on the same runner
  • Step: Individual task within a job
  • Action: Reusable unit of code
  • Runner: Server that runs the workflow

Workflow File Structure

name: CI Pipeline

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

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run a script
        run: echo "Hello, World!"

Setup CI Pipeline for Next.js

Let’s create a complete CI pipeline that includes linting, testing, and building.

1. Basic CI Workflow

Create file .github/workflows/ci.yml:

name: CI

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

jobs:
  lint:
    name: Lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Run ESLint
        run: npm run lint

  test:
    name: Test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Run tests
        run: npm test

  build:
    name: Build
    runs-on: ubuntu-latest
    needs: [lint, test]
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Build
        run: npm run build

2. Type Checking

Add type checking for TypeScript:

  typecheck:
    name: Type Check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Type check
        run: npx tsc --noEmit

Deploy to Vercel

Vercel is a platform created by the Next.js team, so deployment integration is very seamless.

The easiest way is to connect your GitHub repository to Vercel. Every push will automatically trigger a deployment.

  1. Go to vercel.com
  2. Import repository from GitHub
  3. Vercel will auto-detect Next.js and configure settings
  4. Done! Every push to main will deploy to production

Manual Deployment via GitHub Actions

If you need more control, use Vercel CLI:

name: Deploy to Vercel

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      
      - name: Install Vercel CLI
        run: npm i -g vercel@latest
      
      - name: Pull Vercel Environment
        run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
      
      - name: Build Project
        run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
      
      - name: Deploy to Vercel
        run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}

env:
  VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
  VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}

Setup Vercel Secrets

  1. Install Vercel CLI: npm i -g vercel
  2. Login: vercel login
  3. Link project: vercel link
  4. Get token from Vercel Account Settings
  5. Add secrets in GitHub repository settings:
    • VERCEL_TOKEN
    • VERCEL_ORG_ID (from .vercel/project.json)
    • VERCEL_PROJECT_ID (from .vercel/project.json)

Deploy to VPS with Docker

For those who prefer self-hosting, here’s how to deploy to a VPS using Docker.

1. Dockerfile for Next.js

Create Dockerfile in project root:

FROM node:20-alpine AS base

FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED 1
RUN npm run build

FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs
EXPOSE 3000
ENV PORT 3000

CMD ["node", "server.js"]

Make sure next.config.js uses standalone output:

/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'standalone',
}

module.exports = nextConfig

2. GitHub Actions for VPS Deployment

name: Deploy to VPS

on:
  push:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Log in to Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      
      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=sha
            type=raw,value=latest
      
      - name: Build and push Docker image
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}

  deploy:
    needs: build-and-push
    runs-on: ubuntu-latest
    
    steps:
      - name: Deploy to VPS
        uses: appleboy/[email protected]
        with:
          host: ${{ secrets.VPS_HOST }}
          username: ${{ secrets.VPS_USERNAME }}
          key: ${{ secrets.VPS_SSH_KEY }}
          script: |
            docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
            docker stop nextjs-app || true
            docker rm nextjs-app || true
            docker run -d \
              --name nextjs-app \
              -p 3000:3000 \
              --restart unless-stopped \
              -e DATABASE_URL=${{ secrets.DATABASE_URL }} \
              ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest

3. Setup VPS Secrets

Add these secrets in GitHub:

  • VPS_HOST: IP address or domain of VPS
  • VPS_USERNAME: SSH username (usually root or another user)
  • VPS_SSH_KEY: SSH private key

Generate SSH key:

ssh-keygen -t ed25519 -C "github-actions"

Copy public key to VPS:

ssh-copy-id -i ~/.ssh/id_ed25519.pub user@your-vps-ip

Environment Secrets

Never commit secrets to the repository! Use GitHub Secrets.

Adding Secrets

  1. Open repository → Settings → Secrets and variables → Actions
  2. Click “New repository secret”
  3. Enter name and value

Using Secrets in Workflow

steps:
  - name: Build with env
    run: npm run build
    env:
      DATABASE_URL: ${{ secrets.DATABASE_URL }}
      NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }}

Caching for Faster Builds

Caching dependencies and build artifacts is very important to speed up CI/CD.

Cache npm Dependencies

- name: Setup Node.js
  uses: actions/setup-node@v4
  with:
    node-version: '20'
    cache: 'npm'

Cache Next.js Build

- name: Cache Next.js build
  uses: actions/cache@v4
  with:
    path: |
      ~/.npm
      ${{ github.workspace }}/.next/cache
    key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
    restore-keys: |
      ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-

Cache Docker Layers

- name: Build and push Docker image
  uses: docker/build-push-action@v5
  with:
    context: .
    push: true
    tags: ${{ steps.meta.outputs.tags }}
    cache-from: type=gha
    cache-to: type=gha,mode=max

Matrix Builds

Matrix builds allow you to run jobs with various configurations in parallel.

Testing Multiple Node Versions

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18, 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 test

Best Practices

1. Keep Workflows DRY

Use reusable workflows to avoid duplication:

# .github/workflows/reusable-build.yml
name: Reusable Build

on:
  workflow_call:
    inputs:
      node-version:
        required: false
        type: string
        default: '20'

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
          cache: 'npm'
      - run: npm ci
      - run: npm run build

2. Use Concurrency

Cancel previous workflows if there’s a new push:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

3. Use Timeouts

jobs:
  build:
    runs-on: ubuntu-latest
    timeout-minutes: 15

4. Security Best Practices

  • Use permissions to limit token access
  • Pin action versions with SHA: uses: actions/checkout@8ade135
  • Review third-party actions before using
  • Don’t expose secrets in logs
jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

Conclusion

GitHub Actions is a powerful tool for automating CI/CD pipelines for Next.js. With proper setup, you can:

  • Run linting, testing, and type checking automatically
  • Deploy to Vercel with zero-config
  • Deploy to VPS using Docker
  • Secure secrets and environment variables
  • Speed up builds with caching
  • Send notifications to the team

Start with a simple workflow, then iterate based on project needs. Hope this tutorial helps!