Ever spent 20 minutes waiting for your monorepo to build in Docker, only to change one line of code and wait another 20 minutes? Yeah, we've all been there. Let's fix that.
The Monorepo Reality Check
Picture this: You're working on a fantastic monorepo with your React frontend, Express backend, shared UI components, and utility libraries all living in harmony. Everything's great locally with hot reloading and incremental builds. Then you try to containerize it and...
BAM! ๐ฅ
Your build times go from 30 seconds to 15 minutes. Your Docker images balloon to 2GB. Your CI pipeline becomes a coffee break necessity rather than a quick feedback loop.
Sound familiar? Let's dive into how TurboRepo and Docker can work together to solve these pain points.
What Exactly is TurboRepo?
Think of TurboRepo as your monorepo's personal trainer. Created by the brilliant folks at Vercel, it's a high-performance build system that understands your workspace structure and optimizes the heck out of it.
Key Superpowers:
Feature | What It Does | Why You Care |
---|---|---|
Smart Caching | Remembers what you've built before | Skip rebuilding unchanged code |
Parallel Execution | Runs multiple tasks simultaneously | Cut build times in half (or more!) |
Dependency Awareness | Knows which packages depend on what | Only rebuild what actually changed |
Pruning | Extracts only what you need | Smaller Docker contexts = faster builds |
The Docker + Monorepo Problem ๐ค
Let's be honest about what usually happens when you try to dockerize a monorepo:
The Naive Approach
dockerfile1# Don't do this! ๐ฑ 2COPY . . 3RUN npm install 4RUN npm run build
Problems:
-
Copies your entire 500MB workspace (including node_modules, .git, build artifacts)
-
Installs dependencies for ALL packages (even ones you don't need)
-
Rebuilds everything on any file change
-
Creates massive images
Build Time Comparison
Approach | Build Context Size | Build Time | Image Size |
---|---|---|---|
Naive Copy Everything | 500MB+ | 15-20 min | 2GB+ |
With TurboRepo Pruning | 50-100MB | 3-5 min | 400MB |
Optimized Multi-stage | 50-100MB | 2-3 min | 200MB |
These are real numbers from a typical Next.js + Express + shared libs setup
Turbo to the Rescue!
Here's where TurboRepo becomes your best friend. The secret sauce is the turbo prune
command.
How turbo prune
Works
Imagine your monorepo is a massive tree, but you only need one branch for your Docker build:
plaintext1monorepo/ 2โโโ apps/ 3โ โโโ web/ โ You want this 4โ โโโ mobile/ โ But not this 5โ โโโ admin/ โ Or this 6โโโ packages/ 7โ โโโ ui/ โ Need this (web depends on it) 8โ โโโ utils/ โ Need this too 9โ โโโ database/ โ But not this 10โโโ package.json
turbo prune --scope=web
creates a minimal subset containing only:
-
The
web
app -
Dependencies that
web
actually uses (ui
,utils
) -
Root configuration files
-
Proper package.json files with correct dependencies
The Magic Dockerfile
Here's a production-ready Dockerfile that leverages TurboRepo's superpowers:
dockerfile1# Multi-stage build for optimal caching 2FROM node:18-alpine AS base 3 4# Install dependencies only when needed 5FROM base AS deps 6RUN apk add --no-cache libc6-compat 7WORKDIR /app 8 9# Install Turbo 10RUN npm install -g turbo 11 12# Copy the entire repo 13COPY . . 14 15# Prune the monorepo to only include the web app and its dependencies 16RUN turbo prune --scope=web --docker 17 18# Add lockfile and package.json's of isolated subworkspace 19FROM base AS installer 20RUN apk add --no-cache libc6-compat 21WORKDIR /app 22RUN npm install -g turbo 23 24# First install the dependencies (this is done before copying the source code to take advantage of Docker layer caching) 25COPY /app/out/json/ . 26COPY /app/out/package-lock.json ./package-lock.json 27RUN npm ci 28 29# Build the project 30COPY /app/out/full/ . 31COPY turbo.json turbo.json 32 33RUN turbo build --filter=web... 34 35# Production image, copy all the files and run next 36FROM base AS runner 37WORKDIR /app 38 39# Don't run production as root 40RUN addgroup --system --gid 1001 nodejs 41RUN adduser --system --uid 1001 nextjs 42 43# Copy built application 44COPY /app/apps/web/next.config.js . 45COPY /app/apps/web/package.json . 46 47# Automatically leverage output traces to reduce image size 48COPY /app/apps/web/.next/standalone ./ 49COPY /app/apps/web/.next/static ./apps/web/.next/static 50COPY /app/apps/web/public ./apps/web/public 51 52USER nextjs 53 54EXPOSE 3000 55 56ENV PORT 3000 57 58CMD ["node", "apps/web/server.js"]
Breaking Down the Magic
Let me walk you through what's happening here:
-
Stage 1 (deps): We copy everything and run
turbo prune
. This creates a minimal workspace in/app/out/
-
Stage 2 (installer): We install dependencies from the pruned workspace and build only what we need
-
Stage 3 (runner): We create a minimal production image with just the built application
Best Practices That Actually Work
1. Optimize Your .dockerignore
plaintext1# Don't copy these into Docker context 2node_modules 3npm-debug.log* 4.next 5.nuxt 6dist 7.git 8.gitignore 9README.md 10.env 11.nyc_output 12coverage 13.DS_Store
2. Smart Caching Strategy
dockerfile1# Pro tip: Copy package.json files first for better layer caching 2COPY package*.json ./ 3COPY apps/web/package*.json ./apps/web/ 4COPY packages/*/package.json ./packages/ 5RUN npm ci 6 7# Then copy source code 8COPY . .
3. Leverage Turbo's Remote Caching
json1// turbo.json 2{ 3 "extends": ["//"], 4 "pipeline": { 5 "build": { 6 "dependsOn": ["^build"], 7 "outputs": ["dist/**", ".next/**"], 8 "cache": true 9 } 10 }, 11 "remoteCache": { 12 "signature": true 13 } 14}
Real-World Performance Impact
Here's what happened when a team I worked with implemented this approach:
Before TurboRepo + Docker Optimization
plaintext1โโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโฌโโโโโโโโโโโโโโ 2โ Metric โ Before โ Pain Level โ 3โโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโผโโโโโโโโโโโโโโค 4โ Build Time โ 18 min โ โโโ โ 5โ Image Size โ 2.1 GB โ ๐ โ 6โ CI Pipeline โ 25 min โ ๐ด โ 7โ Developer Mood โ ๐ค โ Frustrated โ 8โโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโดโโโโโโโโโโโโโโ
After TurboRepo + Docker Optimization
plaintext1โโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโฌโโโโโโโโโโโโโโ 2โ Metric โ After โ Joy Level โ 3โโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโผโโโโโโโโโโโโโโค 4โ Build Time โ 3.5 min โ ๐ โ 5โ Image Size โ 180 MB โ ๐ฏ โ 6โ CI Pipeline โ 6 min โ โก โ 7โ Developer Mood โ ๐ โ Happy โ 8โโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโดโโโโโโโโโโโโโโ
That's an 80% reduction in build time and 91% reduction in image size!
CI/CD Integration: GitHub Actions Example ๐ง
Here's how to make this work beautifully in your CI pipeline:
yaml1name: Build and Deploy 2on: 3 push: 4 branches: [main] 5 6jobs: 7 build: 8 runs-on: ubuntu-latest 9 steps: 10 - name: Checkout 11 uses: actions/checkout@v3 12 13 - name: Setup Node.js 14 uses: actions/setup-node@v3 15 with: 16 node-version: '18' 17 cache: 'npm' 18 19 - name: Install dependencies 20 run: npm ci 21 22 - name: Build with Turbo 23 run: npx turbo build --filter=web 24 env: 25 TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 26 TURBO_TEAM: ${{ secrets.TURBO_TEAM }} 27 28 - name: Build Docker image 29 run: docker build -t myapp:${{ github.sha }} . 30 31 - name: Deploy 32 run: | 33 # Your deployment logic here 34 echo "Deploying lightning-fast build! โก"
Caching Between Runs
The real magic happens when you enable remote caching:
bash1# In your CI environment 2export TURBO_TOKEN="your-token-here" 3export TURBO_TEAM="your-team-here" 4 5# Subsequent builds will be blazing fast 6npx turbo build --filter=web 7# โ FULL TURBO (cached) ๐ฅ
Common Gotchas (Learn from My Mistakes)
1. Native Dependencies Drama
Problem: Some packages have native dependencies that behave differently between your local machine and Docker Alpine Linux.
Solution:
dockerfile1# Use libc6-compat for better compatibility 2RUN apk add --no-cache libc6-compat 3 4# Or use the full Debian base for complete compatibility 5FROM node:18-bullseye AS base
2. Node Version Mismatches
Problem: Your local Node version differs from Docker, causing subtle build differences.
Solution: Always pin your Node version and keep it consistent:
json1{ 2 "engines": { 3 "node": ">=18.0.0 <19.0.0" 4 } 5}
3. The "It Works on My Machine" Syndrome
Problem: Different behavior between development and production builds.
Solution: Test your Docker build locally:
bash1# Build exactly like production 2docker build -t myapp:test . 3docker run --rm -p 3000:3000 myapp:test 4 5# Compare with local dev 6npm run dev
Advanced Tips for the Brave
1. Multi-App Builds
Want to build multiple apps efficiently?
dockerfile1# Build multiple apps in parallel 2RUN turbo build --filter=web --filter=api --parallel 3 4# Or build them in dependency order 5RUN turbo build --filter=web...
2. Development Docker Setup
Create a dev-friendly Dockerfile:
dockerfile1FROM node:18-alpine AS development 2WORKDIR /app 3COPY package*.json ./ 4RUN npm ci 5COPY . . 6EXPOSE 3000 7CMD ["npm", "run", "dev"]
3. Smart Layer Ordering
dockerfile1# Order layers from least likely to change to most likely 2COPY package*.json ./ # Changes rarely 3RUN npm ci # Expensive but cached 4COPY turbo.json ./ # Changes occasionally 5COPY apps/ ./apps/ # Changes frequently 6RUN turbo build # Only runs if above changed
When Should You Adopt This Setup?
Perfect For:
-
Monorepos with 3+ packages
-
Teams with slow CI/CD pipelines
-
Projects with large Docker images
-
Codebases where small changes trigger full rebuilds
Maybe Overkill For:
-
Single-package repositories
-
Projects already building in under 2 minutes
-
Teams just starting with Docker
-
Legacy codebases with complex build processes
The Future is Bright
The monorepo + containerization space is evolving rapidly:
-
Bun: Lightning-fast package manager that could make builds even faster
-
Deno: Native TypeScript support might simplify our build pipelines
-
BuildKit: Docker's new build system with better caching
-
Remote Development: Dev containers are becoming mainstream
The Real Win: Getting Your Life Back
Here's the thing about TurboRepo + Dockerโit's not just about shaving minutes off your build times (though that 80% reduction is pretty cool). It's about fundamentally changing how you work.
What This Really Means for Your Daily Life
Instead of this frustrating cycle:
plaintext1Write code โ Push โ Wait 20 minutes โ Build fails โ 2Fix tiny issue โ Wait another 20 minutes โ Finally deploys โ 3Repeat tomorrow ๐
You get this smooth flow:
plaintext1Write code โ Push โ Grab coffee โ โ 2Build is done โ Ship to production โ 3Build the next feature ๐
The Compound Effect
Think about it: if you save 15 minutes per build, and you deploy 5 times a day, that's 75 minutes back in your day. Over a month? That's 25 hoursโmore than three full working days!
What could you do with an extra three days every month?
-
Actually finish that side project you've been "working on"
-
Learn that new framework you've been curious about
-
Have lunch away from your desk (revolutionary, I know)
-
Leave work at a reasonable hour and see your family
-
Sleep better knowing your deployments actually work
The Team Multiplier Effect
This isn't just about youโit's about your entire team's momentum:
Before Optimization | After Optimization |
---|---|
"Let's deploy tomorrow, CI takes forever" | "Ship it! We'll know in 5 minutes if it works" |
Pull requests sit for hours waiting for builds | Instant feedback loop |
Friday deployments are banned | Deploy any day with confidence |
Developers context-switch during builds | Stay in the zone, keep coding |
Quick Recap (Your Action Plan):
-
Use
turbo prune
to extract only what you need -
Multi-stage builds for optimal Docker layer caching
-
Smart .dockerignore to reduce build context bloat
-
Remote caching for CI pipeline superpowers
-
Test locally first to avoid production surprises
Your 30-Minute Implementation Checklist:
-
[ ] Add TurboRepo to your monorepo (
npm install turbo -g
) -
[ ] Configure turbo.json with your build pipeline
-
[ ] Replace your Dockerfile with the multi-stage version
-
[ ] Create/update .dockerignore with common exclusions
-
[ ] Enable remote caching (optional but recommended)
-
[ ] Update CI/CD pipeline to use turbo commands
-
[ ] Time your first optimized build and do a happy dance!
The Bottom Line
Every minute spent setting this up pays back 10x within the first week. Your future self (and your teammates) will thank you when they're shipping features instead of watching progress bars.
Remember: The best developers aren't the ones who write the most code, they're the ones who eliminate friction so everyone can focus on what actually matters: building amazing products that users love.
Resources to Dive Deeper:
Remember: The best build system is the one that gets out of your way and lets you ship amazing products. Happy codingโค