Twelve minutes. That's how long our team stared at a spinning CI spinner every time we pushed a change to our monorepo. Twelve minutes of context switching, Slack scrolling, and pretending to review PRs while the Docker daemon churned through layers it had built a hundred times before. We didn't accept it as the cost of doing business. We treated it like a bug.
The Baseline: What We Were Doing Wrong
Our Dockerfile was a textbook example of cargo-culting. We copied the entire monorepo context before installing dependencies. We ran npm install in a single stage for every service. We ignored .dockerignore. The result: no layer caching between builds, massive context transfer over the network, and zero parallelism.
Step 1: Shrink the Build Context
The fastest bytes to transfer are the ones you never send. We audited .dockerignore like it was a security policy. Node modules, git history, local env files, test coverage reports, IDE folders — all gone. Context size dropped from 2.1 GB to 45 MB. Build upload time went from 90 seconds to 3.
Step 2: Leverage Layer Caching Ruthlessly
Docker caches layers in order. If step 3 changes, steps 4+ rerun. We reordered the Dockerfile to copy only package manifests first, install deps, then copy source code. A source change no longer triggers npm ci.
Step 3: Enable BuildKit and Parallelism
Classic Docker builds are sequential. BuildKit runs independent stages concurrently. We added DOCKER_BUILDKIT=1 to CI and restructured stages so deps, lint, and test stages run in parallel. The DAG scheduler handles the rest.
Step 4: Cache Mounts for Package Managers
Even with layer caching, npm ci re-extracts tarballs. BuildKit's --mount=type=cache persists the global npm cache across builds. Cold builds stay fast; warm builds become instant.
Step 5: Multi-Stage for Production Slimness
The final image dropped from 1.2 GB to 180 MB. We copy only dist and production node_modules into a distroless base. Smaller images pull faster, deploy faster, and have a smaller attack surface.
| Metric | Before | After | Improvement |
|---|---|---|---|
| Build Time | 12 min 03 sec | 1 min 28 sec | 88% faster |
| Context Size | 2.1 GB | 45 MB | 98% smaller |
| Final Image | 1.2 GB | 180 MB | 85% smaller |
| CI Cost/Month | $1,450 | $180 | 87% cheaper |
The CI Configuration That Ties It Together
Your 15-Minute Action Plan
Don't rewrite everything. Pick one service. Add .dockerignore. Reorder the Dockerfile for cache. Enable BuildKit. Measure. Repeat. The compounding returns are real: our team saves 400+ engineering hours per month. That's not infrastructure work. That's product velocity.
✦
"The best optimization is deleting code. The second best is deleting wait time.
— Platform Team Lead










