Back to Blog
Performance10 min read2026-07-08

How to Speed Up Docker Builds with Layer Caching

Slow Docker builds kill developer feedback loops and inflate CI minutes. Master layer caching, BuildKit cache mounts, and registry-backed caches to turn multi-minute builds into seconds.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

# 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:

  1. 1The parent layer is unchanged, and
  2. 2The instruction itself is unchanged, and
  3. 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:latest

Cache comparison

StrategySurvives across CI runs?Setup effortBest for
Local layer cacheNoNoneLocal dev
--mount=type=cacheOnly with persistent builderLowDependency downloads
Registry cache (buildx)YesMediumCI/CD
GHA cache backendYesLowGitHub 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 update in 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).

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Performance

Browse all Performance articles →

See also