# How to Speed Up Docker Builds with Layer Caching
A build that takes four minutes might not sound terrible — until you multiply it by every push, every PR, and every developer on the team. Slow builds tax your CI bill and, worse, your patience. Most of that time is spent re-doing work the build system already did. Layer caching is how you stop paying for it twice.
How the layer cache works
Docker builds images instruction by instruction, and each instruction produces a layer. Before running an instruction, the builder checks whether it can reuse a cached layer from a previous build. The cache is valid only if:
- 1The parent layer is unchanged, and
- 2The instruction itself is unchanged, and
- 3For
COPY/ADD, the file contents being copied are unchanged.
The crucial consequence: the moment one layer's cache is invalidated, every layer after it is rebuilt. This is why instruction order matters so much.
Order instructions from least to most volatile
Your application source changes constantly. Your dependencies change rarely. So copy and install dependencies *before* copying source:
# Bad — any code change busts the npm ci cache
COPY . .
RUN npm ci
# Good — npm ci is reused until package*.json changes
COPY package*.json ./
RUN npm ci
COPY . .With the second version, editing a React component reuses the cached npm ci layer and skips a full dependency reinstall. This one change often cuts incremental builds by 80%.
BuildKit cache mounts
The layer cache is binary — a layer is either reused whole or rebuilt whole. But package managers keep their *own* caches (~/.npm, ~/.cache/pip, Go's module cache) that could help even when the layer is rebuilt. [BuildKit](https://docs.docker.com/build/buildkit/) cache mounts persist these across builds without baking them into the image:
# syntax=docker/dockerfile:1
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .Now even when package-lock.json changes and the layer rebuilds, npm pulls already-downloaded tarballs from the cache mount instead of the network. Examples for other ecosystems:
# pip
RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt
# Go
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go build -o /app ./...Make sure BuildKit is enabled (DOCKER_BUILDKIT=1, or default in modern Docker) and the # syntax line is present.
Caching in CI
CI runners are usually ephemeral, so the local layer cache vanishes between runs. You need an external cache. With docker buildx, you can export and import the cache to/from a registry:
docker buildx build \
--cache-from type=registry,ref=registry.example.com/myapp:buildcache \
--cache-to type=registry,ref=registry.example.com/myapp:buildcache,mode=max \
-t registry.example.com/myapp:latest \
--push .mode=max exports cache for all stages (including intermediate build stages), which is what you want for multi-stage Dockerfiles. GitHub Actions users can use the cache-from/cache-to type=gha backend instead:
- uses: docker/build-push-action@v6
with:
cache-from: type=gha
cache-to: type=gha,mode=max
push: true
tags: myapp:latestCache comparison
| Strategy | Survives across CI runs? | Setup effort | Best for |
|---|---|---|---|
| Local layer cache | No | None | Local dev |
--mount=type=cache | Only with persistent builder | Low | Dependency downloads |
Registry cache (buildx) | Yes | Medium | CI/CD |
| GHA cache backend | Yes | Low | GitHub Actions |
Don't accidentally bust the cache
Common ways teams sabotage their own cache:
- Copying the whole context first.
COPY . .near the top invalidates everything downstream on any file change. Copy narrowly. - Embedding timestamps or build args that change every build into early layers.
- Missing
.dockerignore, so unrelated file changes (logs, local artifacts) alter the build context hash. apt-get updatein its own layer, which can serve stale package indexes; combine it with the install.
Parallelize independent stages
BuildKit builds independent stages of a multi-stage Dockerfile concurrently. If you have, say, a frontend build stage and a backend build stage that don't depend on each other, structure them as separate stages and BuildKit will run them in parallel automatically — free speedup with no extra config.
Measure before and after
Use the build timing output and docker buildx build --progress=plain to see which steps are cache hits (CACHED) versus rebuilt. Track the wall-clock time for a no-op rebuild (change one comment) — that's your best signal that caching is working.
How PandaStack handles this
PandaStack builds with rootless BuildKit inside ephemeral Kubernetes Job pods, so cache mounts and multi-stage parallelism work out of the box. Builds stream live logs so you can watch exactly which layers are cached versus rebuilt, and images push to a managed Artifact Registry before a Helm-based deploy. You get the caching wins without wiring up buildx and registry credentials yourself.
References
- [Docker BuildKit documentation](https://docs.docker.com/build/buildkit/)
- [Cache management with buildx](https://docs.docker.com/build/cache/backends/)
- [Optimizing builds with cache (Docker docs)](https://docs.docker.com/build/cache/)
- [docker/build-push-action](https://github.com/docker/build-push-action)
- [Dockerfile reference: RUN --mount](https://docs.docker.com/reference/dockerfile/#run---mounttypecache)
Tired of babysitting CI build times? PandaStack's free tier includes 300 build minutes/month with caching built in. [Try it at dashboard.pandastack.io](https://dashboard.pandastack.io).