Turborepo
Turborepo is a high-performance build system for JavaScript and TypeScript monorepos. It understands the task graph of your monorepo, caches outputs locally and remotely, and only re-runs tasks that have changed. The result is dramatically faster CI and local development.
Installation
# Add to existing monorepo
pnpm add -D -w turbo
npm install --save-dev turbo
yarn add --dev turbo
# Install globally
npm install -g turbo
pnpm add -g turbo
# Create a new Turborepo from scratch
npx create-turbo@latest
pnpm dlx create-turbo@latest
# Create with specific package manager
pnpm dlx create-turbo@latest --package-manager pnpm
# Check version
turbo --version
# Upgrade
npx @turbo/codemod upgrade
Configuration
Turborepo is configured via turbo.json at the monorepo root:
{
"$schema": "https://turbo.build/schema.json",
"ui": "tui",
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", ".env.production"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
},
"test": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", "tests/**"],
"outputs": [],
"cache": true
},
"lint": {
"dependsOn": [],
"inputs": ["$TURBO_DEFAULT$", ".eslintrc.js"],
"outputs": []
},
"dev": {
"cache": false,
"persistent": true
},
"typecheck": {
"dependsOn": ["^typecheck"],
"outputs": []
},
"clean": {
"cache": false
}
}
}
Package-Level Task Override
// apps/web/turbo.json
{
"extends": ["//"],
"tasks": {
"build": {
"env": ["NEXT_PUBLIC_API_URL", "NEXT_PUBLIC_GA_ID"],
"outputs": [".next/**", "!.next/cache/**"]
}
}
}
Environment Variables in Cache Keys
{
"tasks": {
"build": {
"env": ["NODE_ENV", "API_URL"],
"passThroughEnv": ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"],
"outputs": ["dist/**"]
}
},
"globalEnv": ["CI", "TURBO_TEAM"],
"globalPassThroughEnv": ["VERCEL_ENV"]
}
Core Commands
| Command | Description |
|---|---|
turbo run build | Run build across all packages |
turbo build | Shorthand for turbo run build |
turbo run build test lint | Run multiple tasks |
turbo run build --filter=web | Run in specific package |
turbo run build --filter="./apps/*" | Run in matching packages |
turbo run build --filter="...[HEAD^1]" | Run on changed packages |
turbo run build --dry-run | Preview what would run |
turbo run build --dry-run=json | Preview as JSON output |
turbo run build --force | Ignore cache, re-run all |
turbo run build --no-cache | Run without writing to cache |
turbo run build --output-logs=new-only | Show only non-cached output |
turbo run build --output-logs=none | Suppress all output |
turbo run build --concurrency=4 | Limit parallel tasks |
turbo run build --graph | Generate task dependency graph |
turbo run build --graph=output.png | Save graph as image |
turbo run dev --parallel | Run all dev tasks in parallel |
turbo login | Log in to Vercel Remote Cache |
turbo logout | Log out of Remote Cache |
turbo link | Link to Vercel Remote Cache |
turbo unlink | Unlink from Remote Cache |
turbo prune --scope=web | Prune monorepo for a package |
turbo generate | Run a workspace generator |
turbo daemon status | Check Turborepo daemon status |
Advanced Usage
Dependency Graph (dependsOn)
{
"tasks": {
"build": {
"dependsOn": ["^build"]
}
}
}
| Pattern | Meaning |
|---|---|
"^build" | Run build in all dependency packages first (topological) |
"build" | Run build in the same package first |
"ui#build" | Run build in the ui package specifically first |
[] | No dependencies — run immediately |
Remote Caching
# Vercel Remote Cache (free with Vercel account)
turbo login
turbo link
# Set team slug for shared cache
TURBO_TEAM=my-team turbo run build
TURBO_TOKEN=your-token TURBO_TEAM=my-team turbo run build
# Self-hosted remote cache (open source)
# Set environment variables:
export TURBO_API="https://my-cache.example.com"
export TURBO_TEAM="my-team"
export TURBO_TOKEN="my-token"
turbo run build
Pruning for Docker
# Create a minimal monorepo subset for one app
turbo prune web --docker
# Output structure:
# out/
# json/ — package.json files only (for dep install layer)
# full/ — full source (for build layer)
# yarn.lock (or pnpm-lock.yaml)
FROM node:20-alpine AS pruner
RUN npm install -g turbo
WORKDIR /app
COPY . .
RUN turbo prune web --docker
FROM node:20-alpine AS installer
WORKDIR /app
COPY --from=pruner /app/out/json/ .
COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
RUN npm install -g pnpm && pnpm install --frozen-lockfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=installer /app/node_modules ./node_modules
COPY --from=pruner /app/out/full/ .
RUN pnpm turbo run build --filter=web
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=builder /app/apps/web/.next ./.next
EXPOSE 3000
CMD ["node", "server.js"]
Filtering Syntax
# By package name
turbo run test --filter=my-app
# By glob (must quote)
turbo run lint --filter="./packages/*"
turbo run lint --filter="./apps/web"
# Changed since a git ref
turbo run test --filter="...[HEAD~1]" # changed in last commit
turbo run test --filter="...[main]" # changed vs main branch
turbo run test --filter="...[origin/main]"
# Dependents (packages that depend on X)
turbo run build --filter="...^my-lib" # apps that use my-lib
# Dependencies of a package
turbo run build --filter="my-app^..." # build deps of my-app
# Exclude a package
turbo run build --filter='!./packages/legacy'
Workspace Generators
# List available generators
turbo generate
# Create a new package from template
turbo generate workspace --name my-new-package
# Custom generators live in turbo/generators/
# config.ts defines the generator interface
CI with GitHub Actions
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
name: Build and Test
runs-on: ubuntu-latest
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # needed for --filter="...[HEAD~1]"
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm turbo run lint typecheck build test
Common Workflows
Local Development
# Run dev servers for all apps simultaneously
turbo run dev
# Run dev only for specific app and its deps
turbo run dev --filter=web...
# Build and watch a library as you develop
turbo run build --filter=ui --watch
Checking Cache Hit Rate
# See cache status with summary
turbo run build --output-logs=new-only
# Full run stats
turbo run build --summarize
# Dry run to preview (no actual execution)
turbo run build --dry-run=json | jq '.packages[].tasks[] | {task: .taskId, cached: .cache.local}'
Affected Package Builds in PRs
# Only run tasks on packages changed in this PR vs main
turbo run build test --filter="...[origin/main]"
# Include their dependents too (apps that use changed packages)
turbo run build test --filter="...^[origin/main]..."
Tips and Best Practices
Start with turbo.json outputs — Correctly specifying outputs is the most important thing for cache correctness. If you don’t list an output directory, Turborepo can’t restore it from cache.
Use $TURBO_DEFAULT$ for inputs — This special token includes all files tracked by git that aren’t in a .gitignore, plus any files you add. It’s the correct default — don’t use ["**/*"] which is too broad.
Set "cache": false for dev tasks — Development servers have side effects (ports, watchers) and should never be cached. Always set "persistent": true as well so Turbo doesn’t treat them as finished tasks.
Enable Remote Cache early — Even on solo projects, Vercel’s free remote cache shares builds between your local machine and CI. This dramatically speeds up CI for large monorepos.
Use --output-logs=new-only — In CI, suppress cached task output to keep logs readable. You’ll still see all failures.
Avoid glob-heavy inputs — Specific inputs patterns produce more cache hits. If your lint task only depends on *.ts files, say so — changes to .md files won’t bust the cache.
Prune for Docker builds — Always use turbo prune when building Docker images. It creates a minimal monorepo subset with no unnecessary packages, producing much smaller layers and faster builds.
Use the TUI — Set "ui": "tui" in turbo.json to enable the interactive terminal UI that shows task progress in real time with per-package status.