Why Turborepo?
Running pnpm build in a monorepo with 20 packages rebuilds everything every time. Turborepo understands your task dependency graph, caches outputs by input hash, and skips tasks whose inputs have not changed — locally and in CI.
Initial Setup
pnpm add -D turbo -w # -w installs to workspace root
# Or scaffold a new monorepo
npx create-turbo@latest
// turbo.json (root)
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "dist/**"]
},
"lint": {
"dependsOn": ["^build"]
},
"test": {
"dependsOn": ["^build"],
"outputs": ["coverage/**"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}
The "^build" syntax means "run build in all packages this package depends on first."
Task Graph in Practice
Given a monorepo with web depending on ui and utils:
apps/web → packages/ui → packages/utils
Running turbo build automatically:
- Builds
utilsfirst - Builds
ui(can now resolveutils) - Builds
web(can now resolveuiandutils)
All independent packages build in parallel.
Remote Cache
The remote cache stores task outputs on a server. When a CI run hits the same input hash, it downloads the cached output instead of running the task.
Vercel Remote Cache (free with Vercel account):
npx turbo login
npx turbo link
Self-hosted with Ducktape:
# Deploy Ducktape (open-source Turborepo remote cache)
docker run -p 3000:3000 ghcr.io/ducktape/ducktape:latest
# Use in CI
TURBO_API=http://your-cache-server:3000 TURBO_TOKEN=your-token TURBO_TEAM=your-team turbo build
Pruning for Docker Layers
turbo prune creates a minimal subset of your monorepo containing only the packages needed for a specific app — ideal for Docker:
FROM node:22-alpine AS pruner
WORKDIR /app
RUN pnpm add -g turbo
COPY . .
RUN turbo prune web --docker
FROM node:22-alpine AS installer
WORKDIR /app
COPY --from=pruner /app/out/json/ .
COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
RUN pnpm install --frozen-lockfile
FROM node:22-alpine AS builder
WORKDIR /app
COPY --from=installer /app .
COPY --from=pruner /app/out/full/ .
RUN turbo build --filter=web
FROM node:22-alpine AS runner
COPY --from=builder /app/apps/web/.next/standalone ./
CMD ["node", "server.js"]
Filtering for Affected Packages
Only build packages affected by changes on the current branch:
# Build only changed packages and their dependents
turbo build --filter="...[origin/main]"
# Build a specific app and all its dependencies
turbo build --filter=web...
Turborepo vs Nx
| Feature | Turborepo | Nx | |---------|-----------|-----| | Config complexity | Simple JSON | More complex | | Plugin ecosystem | Smaller | Larger | | Code generation | No | Yes | | Affected detection | Git-based | Git + dep graph | | Remote cache | Vercel or self-hosted | Nx Cloud or self-hosted |
Turborepo is better for teams that want minimal config and are already on pnpm workspaces. Nx is better for large enterprise monorepos that need code generation and extensive scaffolding.
References: Turborepo · GitHub · remote caching