A slow build is a tax you pay on every single commit — multiply a 10-minute build by dozens of pushes a day across a team and it's real money and real lost focus. The good news: most slow builds are slow for a handful of fixable reasons. This guide ranks the highest-leverage fixes for large-app builds.
First, measure where the time goes
Don't optimize blind. Get a per-step timing breakdown. With BuildKit:
DOCKER_BUILDKIT=1 docker build --progress=plain -t myapp .The plain output shows how long each step takes. Almost always, one or two steps dominate — usually dependency installation or asset compilation. Optimize those; ignore the 200 ms steps.
Technique 1: Order layers for caching (highest leverage)
This is the single biggest win and the most commonly botched. Docker/BuildKit caches each layer; a layer's cache is invalidated when it or any layer before it changes. The mistake:
# BAD: copying everything before installing
COPY . .
RUN npm ci # re-runs on ANY source changeHere, changing a single source file busts the COPY . . layer, which busts npm ci — so you reinstall all dependencies on every commit. The fix is to copy dependency manifests first:
# GOOD: deps cached across source changes
COPY package*.json ./
RUN npm ci # cached unless package*.json changes
COPY . . # source changes only bust from here downNow npm ci runs only when dependencies actually change. For a large dependency tree, this alone can cut minutes per build.
Technique 2: Cache mounts for package managers
Even when a dependency install must re-run, you can avoid re-downloading everything using BuildKit cache mounts — a persistent cache that survives across builds without bloating the image:
# syntax=docker/dockerfile:1
RUN --mount=type=cache,target=/root/.npm npm ciThe same pattern works across ecosystems:
| Ecosystem | Cache mount target |
|---|---|
| npm | /root/.npm |
| pnpm | /root/.local/share/pnpm/store |
| pip | /root/.cache/pip |
| Go | /root/.cache/go-build, /go/pkg/mod |
| Maven | /root/.m2 |
| apt | /var/cache/apt |
This is especially valuable in ephemeral-build-pod architectures (like PandaStack's) where there's no persistent local disk between builds — the cache mount provides the speed of caching without persisting pod state.
Technique 3: Shrink the build context
The build context is everything sent to the builder. If you accidentally ship node_modules, .git, build outputs, and large assets, you waste time just transferring them. Use .dockerignore:
# .dockerignore
node_modules
.git
dist
*.log
coverage
.nextA bloated context can add seconds-to-minutes before the build even starts, and can subtly bust caches. This is a 30-second fix with outsized payoff.
Technique 4: Multi-stage builds
Multi-stage builds let you keep heavy build tooling out of the final image and let BuildKit prune stages that don't contribute to your target:
FROM node:20 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-slim
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
CMD ["node", "dist/server.js"]Bonus: a smaller final image also reduces *deploy* time and cold starts, not just build time.
Technique 5: Exploit parallelism
BuildKit builds independent stages in parallel automatically (it models the build as a DAG). You can help it by structuring independent work into separate stages — e.g. building a frontend and backend in parallel stages that merge at the end. Don't force artificial serialization between things that don't depend on each other.
Technique 6: Dependency hygiene
Sometimes the build is slow because you're installing too much:
- Prune unused dependencies. Large
node_modulesinstalls slowly. Audit periodically. - Use production installs in the final stage (
npm ci --omit=dev) so dev tooling isn't installed where it isn't needed. - Commit a lockfile. Beyond determinism, lockfiles let package managers skip resolution work.
- Watch postinstall scripts. A heavy
postinstall(downloading binaries, compiling native modules) can dominate. Cache or eliminate it.
Technique 7: Right-size build resources
A build starved of CPU or memory is slow — or fails outright. The classic symptom is exit code 137 (OOM-killed) during a memory-hungry bundling step (large webpack/Vite builds, TypeScript compilation). If your build OOMs or crawls, give it a larger build tier. This is the one case where throwing resources at the problem is the correct fix.
Priority order (do these in sequence)
- 1
.dockerignore— 30 seconds, immediate. - 2Layer ordering — copy manifests before source.
- 3Cache mounts — for the package manager.
- 4Multi-stage build — smaller image, faster deploy too.
- 5Dependency hygiene — prune, prod-only, lockfile.
- 6Resources — only if measurement shows you're starved.
Measure after each change. The first two fixes alone resolve the majority of slow builds.
How PandaStack helps
PandaStack builds with rootless BuildKit in ephemeral pods, so you automatically get BuildKit's DAG parallelism and layer caching, plus registry-backed and cache-mount caching that works even though build pods are throwaway. You also get live, searchable build logs (self-hosted Elasticsearch) with the --progress=plain-style per-step output you need to find your slow step, and the ability to bump the build tier if a step is resource-starved.
References
- [Docker build cache](https://docs.docker.com/build/cache/)
- [BuildKit cache mounts](https://docs.docker.com/build/cache/optimize/#use-cache-mounts)
- [Dockerfile best practices](https://docs.docker.com/build/building/best-practices/)
- [Multi-stage builds](https://docs.docker.com/build/building/multi-stage/)
- [
.dockerignorereference](https://docs.docker.com/reference/dockerfile/#dockerignore-file)
---
PandaStack gives you BuildKit caching, parallel builds, and per-step build logs out of the box — plus 300 build minutes/month on the free tier to tune your Dockerfile against. Push a repo at [dashboard.pandastack.io](https://dashboard.pandastack.io).