CI/CD for Monorepos: Turborepo and Nx Pipelines
Build efficient CI/CD pipelines for monorepos using Turborepo and Nx with smart caching, affected-only builds, and parallel execution.
What you'll learn
- ✓Structure CI/CD pipelines for monorepo projects
- ✓Configure Turborepo remote caching in CI
- ✓Set up Nx affected commands to only build what changed
- ✓Optimize pipeline speed with parallelism and caching
Prerequisites
- •Basic monorepo concepts
- •GitHub Actions experience
- •Node.js and npm/pnpm knowledge
Monorepos hold multiple packages and applications in a single repository. This structure simplifies dependency management and code sharing, but it creates a CI/CD challenge: you do not want to rebuild and redeploy every package when only one file changed. Without smart tooling, monorepo pipelines become painfully slow.
Turborepo and Nx solve this problem with dependency-aware task execution, intelligent caching, and affected-only filtering. This guide shows you how to set up efficient CI/CD pipelines using both tools.
Why Monorepo CI/CD Is Different
In a polyrepo setup, each repository has its own pipeline. A change triggers exactly one pipeline. In a monorepo, a single commit might touch code across multiple packages, and those packages depend on each other. You need a pipeline that understands the dependency graph so it can build packages in the right order, skip packages that have not changed, and cache results to avoid redundant work.
Project Structure
Both examples in this guide use a similar monorepo structure:
my-monorepo/
apps/
web/ # Next.js frontend
api/ # Express API
admin/ # Admin dashboard
packages/
ui/ # Shared React components
config/ # Shared configuration
utils/ # Shared utilities
tsconfig/ # Shared TypeScript configs
package.json
turbo.json # or nx.json
Turborepo Pipeline Setup
Turborepo uses a turbo.json configuration file to define task dependencies and caching behavior.
Configuring turbo.json
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
},
"lint": {
"dependsOn": ["^build"]
},
"test": {
"dependsOn": ["^build"],
"outputs": ["coverage/**"]
},
"typecheck": {
"dependsOn": ["^build"]
},
"deploy": {
"dependsOn": ["build", "test", "lint"],
"cache": false
}
}
}
The ^build syntax means “run build in all dependencies first.” This ensures packages are built in topological order.
GitHub Actions with Turborepo
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
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: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm turbo build
- name: Lint
run: pnpm turbo lint
- name: Test
run: pnpm turbo test
- name: Typecheck
run: pnpm turbo typecheck
Turborepo Remote Caching
Remote caching is Turborepo’s killer feature for CI. When one developer or CI run builds a package, the result is cached remotely. The next pipeline run that needs the same build skips it entirely.
# Enable Vercel Remote Cache (easiest option)
npx turbo login
npx turbo link
# Or use a self-hosted remote cache
# Set these in your CI environment
export TURBO_API="https://turbo-cache.example.com"
export TURBO_TOKEN="your-cache-token"
export TURBO_TEAM="your-team"
Filtered Builds with Turborepo
When you only want to build and deploy a specific app:
jobs:
deploy-web:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Check for changes
id: changes
run: |
CHANGED=$(pnpm turbo build --filter='web...[HEAD^]' --dry-run=json | jq '.packages | length')
if [ "$CHANGED" -gt "0" ]; then
echo "has_changes=true" >> "$GITHUB_OUTPUT"
else
echo "has_changes=false" >> "$GITHUB_OUTPUT"
fi
- name: Build web app
if: steps.changes.outputs.has_changes == 'true'
run: pnpm turbo build --filter=web...
- name: Deploy web app
if: steps.changes.outputs.has_changes == 'true'
run: pnpm turbo deploy --filter=web
Nx Pipeline Setup
Nx takes a different approach with its project graph analysis and affected commands. It can determine exactly which projects are affected by a code change.
Configuring nx.json
{
"$schema": "https://nx.dev/reference/nx-json",
"namedInputs": {
"default": ["{projectRoot}/**/*", "sharedGlobals"],
"sharedGlobals": [],
"production": [
"default",
"!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
"!{projectRoot}/tsconfig.spec.json",
"!{projectRoot}/jest.config.[jt]s"
]
},
"targetDefaults": {
"build": {
"dependsOn": ["^build"],
"inputs": ["production", "^production"],
"cache": true
},
"lint": {
"inputs": ["default", "{workspaceRoot}/.eslintrc.json"],
"cache": true
},
"test": {
"inputs": ["default", "^production"],
"cache": true
}
},
"defaultBase": "main"
}
Project Configuration
Each project in an Nx monorepo has its own project.json:
{
"name": "web",
"sourceRoot": "apps/web/src",
"projectType": "application",
"targets": {
"build": {
"executor": "@nx/next:build",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/apps/web"
}
},
"serve": {
"executor": "@nx/next:server",
"options": {
"buildTarget": "web:build"
}
},
"lint": {
"executor": "@nx/eslint:lint",
"options": {
"lintFilePatterns": ["apps/web/**/*.{ts,tsx}"]
}
},
"test": {
"executor": "@nx/jest:jest",
"options": {
"jestConfig": "apps/web/jest.config.ts"
}
}
}
}
GitHub Actions with Nx Affected
The nx affected command is the key to efficient Nx pipelines. It compares the current code against a base branch and only runs tasks for projects that have been affected by the changes:
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for affected detection
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- uses: nrwl/nx-set-shas@v4
- name: Lint affected projects
run: npx nx affected -t lint --parallel=3
- name: Test affected projects
run: npx nx affected -t test --parallel=3 --ci --code-coverage
- name: Build affected projects
run: npx nx affected -t build --parallel=3
Nx Cloud for Distributed Caching
Nx Cloud provides remote caching and distributed task execution:
# Connect your workspace to Nx Cloud
npx nx connect
# Add to your CI workflow
- name: Build with Nx Cloud
run: npx nx affected -t build --parallel=3
env:
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
Distributed Task Execution with Nx
For large monorepos, Nx can distribute tasks across multiple CI agents:
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
main:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- uses: nrwl/nx-set-shas@v4
- name: Start Nx Agents
run: npx nx-cloud start-ci-run --distribute-on="3 linux-medium-js"
- name: Run all tasks
run: |
npx nx affected -t lint test build --parallel=3
- name: Stop Nx Agents
if: always()
run: npx nx-cloud stop-all-agents
Deploying Individual Apps
In a monorepo, you often need to deploy individual applications independently. Here is a pattern that detects which apps changed and deploys only those:
name: Deploy
on:
push:
branches: [main]
jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
web: ${{ steps.filter.outputs.web }}
api: ${{ steps.filter.outputs.api }}
admin: ${{ steps.filter.outputs.admin }}
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/utils/**'
admin:
- 'apps/admin/**'
- 'packages/ui/**'
- 'packages/utils/**'
deploy-web:
needs: detect-changes
if: needs.detect-changes.outputs.web == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: echo "Deploying web app..."
deploy-api:
needs: detect-changes
if: needs.detect-changes.outputs.api == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: echo "Deploying API..."
deploy-admin:
needs: detect-changes
if: needs.detect-changes.outputs.admin == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: echo "Deploying admin dashboard..."
Turborepo vs Nx: Which to Choose
Both tools solve the same fundamental problem but differ in philosophy:
Turborepo is focused on being a fast, simple build system. It has fewer concepts to learn, integrates easily with existing projects, and its remote caching through Vercel is straightforward. Choose Turborepo when you want minimal configuration and your monorepo is primarily JavaScript/TypeScript.
Nx is a full-featured monorepo toolkit with code generation, project graph visualization, and distributed task execution. It supports multiple languages and has deeper IDE integration. Choose Nx when you need advanced features like code generators, have a large team, or work with multiple programming languages.
Wrapping Up
Efficient monorepo CI/CD comes down to three capabilities: understanding the dependency graph so tasks run in the right order, caching build results so work is never repeated, and detecting affected projects so unchanged code is skipped entirely. Both Turborepo and Nx provide these capabilities with different tradeoffs. Start with whichever tool matches your team’s complexity needs, enable remote caching from day one, and structure your pipeline to deploy apps independently based on what actually changed.
Related articles
- CI/CD GitHub Actions vs Jenkins: CI/CD Compared
Compare GitHub Actions and Jenkins for CI/CD pipelines. Covers setup, configuration, plugins, pricing, and when to choose each tool for your team.
- CI/CD CI/CD Artifact Management: Build, Store, and Promote
Learn how to manage build artifacts in CI/CD pipelines including versioning, storage, promotion strategies, and cleanup policies.
- CI/CD Optimize Docker Builds in CI/CD Pipelines
Speed up Docker builds in CI/CD — multi-stage builds, layer caching, BuildKit, registry caching, and slim base images for faster deployments.
- CI/CD GitHub Actions: Complete CI/CD Guide for Any Project
Set up GitHub Actions CI/CD from scratch — workflows, jobs, caching, secrets, matrix builds, deployment, and reusable patterns for any language or framework.