← Back to articles

How to Set Up CI/CD for a Monorepo (2026)

Monorepos are great for code sharing but painful for CI/CD. Without proper setup, every PR triggers builds for every package. Here's how to set up fast, efficient CI/CD for monorepos in 2026.

The Problem

monorepo/
├── apps/web/          # Next.js frontend
├── apps/api/          # Hono API
├── packages/ui/       # Shared components
├── packages/utils/    # Shared utilities
└── packages/db/       # Database schema

Change one line in packages/utils/ → rebuild everything? No. You should only rebuild what's affected.

Step 1: Turborepo for Task Orchestration

npx create-turbo@latest
// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "dist/**"]
    },
    "lint": {
      "dependsOn": ["^build"]
    },
    "test": {
      "dependsOn": ["^build"]
    },
    "typecheck": {
      "dependsOn": ["^build"]
    }
  }
}

Turborepo understands your dependency graph. Change packages/ui → only rebuild apps/web (which depends on it), not apps/api.

Step 2: GitHub Actions Workflow

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

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

jobs:
  ci:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2  # Need history for change detection

      - uses: pnpm/action-setup@v4
        with:
          version: 9

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

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      # Turborepo Remote Cache
      - name: Build
        run: pnpm turbo build --filter=...[HEAD^1]
        env:
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
          TURBO_TEAM: ${{ secrets.TURBO_TEAM }}

      - name: Lint
        run: pnpm turbo lint --filter=...[HEAD^1]

      - name: Type Check
        run: pnpm turbo typecheck --filter=...[HEAD^1]

      - name: Test
        run: pnpm turbo test --filter=...[HEAD^1]

Key: --filter=...[HEAD^1]

This tells Turborepo: "Only run tasks for packages that changed since the last commit." On PRs, this means only affected packages are built, linted, and tested.

Step 3: Remote Caching

Turborepo's remote cache shares build artifacts between CI runs and developers.

With Vercel (Easiest)

npx turbo login
npx turbo link

Add TURBO_TOKEN and TURBO_TEAM to GitHub Actions secrets.

Self-Hosted (Free)

# Use turbo-remote-cache-server
docker run -p 3000:3000 ducktors/turborepo-remote-cache

Impact: First CI run: 3 minutes. Subsequent (cached): 30 seconds.

Step 4: Affected-Only Deployment

Deploy Only Changed Apps

# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  detect-changes:
    runs-on: ubuntu-latest
    outputs:
      web: ${{ steps.filter.outputs.web }}
      api: ${{ steps.filter.outputs.api }}
    steps:
      - uses: actions/checkout@v4
      - uses: dorny/paths-filter@v3
        id: filter
        with:
          filters: |
            web:
              - 'apps/web/**'
              - 'packages/ui/**'
              - 'packages/utils/**'
            api:
              - 'apps/api/**'
              - 'packages/db/**'
              - 'packages/utils/**'

  deploy-web:
    needs: detect-changes
    if: needs.detect-changes.outputs.web == 'true'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npx vercel --prod --yes
        env:
          VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}

  deploy-api:
    needs: detect-changes
    if: needs.detect-changes.outputs.api == 'true'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: flyctl deploy --app my-api
        env:
          FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}

Change apps/web → only deploy web. Change packages/utils → deploy both (it's a shared dependency).

Step 5: PR Checks with Status Badges

  ci:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        task: [build, lint, typecheck, test]
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: 'pnpm'
      - run: pnpm install --frozen-lockfile
      - run: pnpm turbo ${{ matrix.task }} --filter=...[origin/main]

Each task (build, lint, typecheck, test) runs as a separate job. PRs show individual status checks.

Common Patterns

Shared Environment Variables

env:
  DATABASE_URL: ${{ secrets.DATABASE_URL }}
  # Only inject where needed

Database Migrations on Deploy

  deploy-api:
    steps:
      - run: pnpm --filter @repo/db migrate
      - run: flyctl deploy

Preview Deployments per App

  preview-web:
    if: github.event_name == 'pull_request'
    steps:
      - run: npx vercel --yes  # Creates preview URL

Performance Tips

  1. pnpm over npm/yarn — faster installs, better monorepo support
  2. Remote cache — don't rebuild what hasn't changed
  3. Parallel jobs — run build, lint, test in parallel
  4. --filter — only process affected packages
  5. Dependency caching — cache node_modules between runs

Typical CI Times

Without OptimizationWith Optimization
Install: 2 minInstall: 30s (cached)
Build: 5 minBuild: 45s (affected + remote cache)
Lint: 2 minLint: 15s (affected)
Test: 3 minTest: 30s (affected)
Total: 12 minTotal: 2 min

FAQ

Turborepo vs Nx for CI?

Both work well. Turborepo is simpler (fewer concepts). Nx has more features (distributed task execution, affected graph). For most teams: Turborepo.

Do I need remote caching?

For teams of 2+, absolutely. It prevents rebuilding packages that another developer (or CI run) already built.

How do I handle database migrations?

Run migrations as a separate step before deployment. Use the packages/db package to centralize migration logic.

Bottom Line

Monorepo CI/CD in 2026: Turborepo for task orchestration, GitHub Actions for CI, remote caching for speed, path filters for affected-only deployment. Setup takes 1-2 hours. CI times drop from 12 minutes to 2 minutes. The key insight: only build, test, and deploy what changed.

Get AI tool guides in your inbox

Weekly deep-dives on the best AI coding tools, automation platforms, and productivity software.