Skip to content
compiler.dev

Next.js GitHub Actions Workflow Template

Last updated

This is a working .github/workflows/ci.yml for a Next.js app on npm. It caches npm downloads and the .next/cache build cache, then runs lint, a type check, tests and a production build. Edit the script names to match your package.json.

The workflow

name: CI

on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read

concurrency:
  group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
  ci:
    runs-on: ubuntu-latest
    timeout-minutes: 20
    env:
      NEXT_TELEMETRY_DISABLED: "1"
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm

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

      - run: npm ci
      - run: npm run lint
      - run: npx tsc --noEmit
      - run: npm test --if-present
      - run: npm run build

What the non-obvious parts do

  • cache: npm caches npm's download folder, keyed on the lockfile. See caching npm.
  • The .next/cache step is the one most templates miss. Next.js stores compiled output and image optimisation results there. Without a restored cache every build starts cold, and Next.js prints a warning about it. The key combines the lockfile hash and a hash of source files, so a code change produces a new entry, while restore-keys falls back to the most recent older one for an incremental build.
  • npx tsc --noEmit type checks without writing files. next build also type checks, but running it first gives a faster, clearer failure.
  • NEXT_TELEMETRY_DISABLED switches off anonymous telemetry in CI.
  • concurrency cancels older runs on the same pull request. Main is never cancelled.
  • timeout-minutes caps a stuck build.

Variants

Environment variables at build time

Variables prefixed with NEXT_PUBLIC_ are inlined at build. Pass them with env: on the build step and keep real secrets out of the client bundle.

End-to-end tests with Playwright

Add npx playwright install --with-deps chromium and npx playwright test. Shard it with a matrix; see test sharding and slow Playwright tests.

Monorepo with Turborepo

Run npx turbo run lint test build with TURBO_TOKEN and TURBO_TEAM set for a remote build cache. More in Nx and Turborepo CI.

Deploy job

Add a second job with needs: ci and if: github.ref == 'refs/heads/main'. Vercel users can skip this and let Vercel's Git integration deploy.

macOS

Rarely useful for a web app. Stay on Linux.

Run it on faster machines

Next.js builds are CPU hungry because SWC and type checking use several cores. Change one line:

    runs-on: [self-hosted, Linux, compiler-ci]

That moves the job to an 8 vCPU machine from compiler.dev, with a fallback to your current runner. The full tuning list is in speed up Next.js on GitHub Actions.

FAQ

Why does my Next.js build warn about no cache?

The .next/cache folder was empty. Add the cache step above and the warning goes away on the next run that restores it.

Should I cache node_modules for Next.js?

No. Cache npm's downloads and let npm ci rebuild node_modules.

Can I use pnpm or Yarn?

Yes. Change cache: and the install command, and update the lockfile name in the key. See pnpm caching.

Do I need Docker for CI?

No. Build in the job. Use Docker only if you deploy a container; see Docker layer caching.