Docker Layer Caching vs BuildKit Cache Mounts
You want faster container builds in CI and are unsure whether ordering Dockerfile instructions for layer caching is enough, or whether you also need BuildKit cache mounts β this explains what each does, where each falls short, and why production Dockerfiles use both.
When to use each β the short version
- Layer caching always: order instructions so unchanged steps (dependency install) are reused when their inputs have not changed.
- BuildKit cache mounts additionally: keep the package managerβs download cache warm so that when the install layer does rebuild, it does not re-download every package.
Prerequisites
Complete working example
# syntax=docker/dockerfile:1.7
# A build that uses BOTH layer caching and a BuildKit cache mount.
FROM node:20-slim AS build
WORKDIR /app
RUN corepack enable
# --- Layer caching: copy the lockfile FIRST so this layer is reused
# on any source-only change (the install layer's inputs are unchanged). ---
COPY pnpm-lock.yaml package.json ./
# --- BuildKit cache mount: persist pnpm's content-addressable store across builds.
# Even when this RUN layer must rebuild (lockfile changed), packages already
# downloaded are reused from the mount instead of re-fetched from the network. ---
RUN \
pnpm install --frozen-lockfile
# Source changes invalidate from here down, but NOT the install layer above.
COPY . .
RUN \
pnpm build
FROM node:20-slim AS runtime
WORKDIR /app
COPY /app/dist ./dist
COPY /app/node_modules ./node_modules
CMD ["node", "dist/server.js"]Build it in CI with a registry layer cache so both mechanisms persist across runners:
docker buildx build \
--cache-from type=registry,ref=ghcr.io/acme/app:buildcache \
--cache-to type=registry,ref=ghcr.io/acme/app:buildcache,mode=max \
-t ghcr.io/acme/app:$GIT_SHA --push .Step-by-step walkthrough
Why layer caching alone is not enough. Layer caching reuses the install layer only while its inputs are unchanged β the moment the lockfile changes, that layer and everything after it rebuilds. The rebuild re-runs pnpm install, which without help re-downloads every package from the network, the slowest part of the install. Layer caching is all-or-nothing at the layer boundary; it cannot make a re-run of install cheaper.
What the cache mount adds. --mount=type=cache,target=β¦/pnpm/store mounts a persistent directory into the RUN step only. It is not part of the resulting image layer β it is scratch space that BuildKit keeps between builds. So when the install layer must rebuild after a lockfile change, pnpm finds most packages already in the mounted store and only downloads the delta. The two mechanisms are complementary: layer caching skips install entirely when it can; the cache mount makes install fast when it cannot be skipped.
Instruction order still matters. The cache mount does not replace ordering. Copying the lockfile before the source (and the source after) is what lets a source-only change reuse the install layer at all β the classic Docker layer caching rule. The mount then optimizes the case ordering cannot help: an actual dependency change.
Registry cache for ephemeral CI. On throwaway CI runners, both the layer cache and the mount would be cold every build without a persistent backend. --cache-to/--cache-from type=registry,mode=max pushes and pulls all layers (including intermediate ones) to a registry, so a fresh runner starts warm β the pattern from Redis-backed and remote CI caching applied to Docker.
Verification
# 1. Source-only change β install layer reused (layer caching).
# Edit a source file, rebuild, and look for "CACHED" on the install RUN step.
docker buildx build --cache-from type=registry,ref=ghcr.io/acme/app:buildcache -t app:test .
# β "=> CACHED [build 4/6] RUN --mount=... pnpm install"
# 2. Lockfile change β install re-runs but is fast (cache mount).
# Add one dependency, rebuild, and confirm install completes in seconds, not minutes,
# because only the new package is downloaded.Expected: an unchanged lockfile keeps the install layer CACHED; a changed lockfile re-runs install but reuses the download store, so it finishes quickly.
Common pitfalls
- Expecting the cache mount to speed unchanged builds. If the install layer is already
CACHED, the mount does nothing β that case is handled by layer caching. The mount only helps when install re-runs. - Omitting
mode=maxon the registry cache. The defaultmode=minexports only final layers, so intermediate layers (like install) are cold on a fresh runner. Usemode=maxto persist them. - Forgetting the
# syntaxdirective. Cache mounts require the BuildKit Dockerfile frontend; without# syntax=docker/dockerfile:1.7(or BuildKit enabled),--mount=type=cacheis not recognized. See the parent guide for BuildKit setup.
Related
- Docker Layer Caching for Full-Stack Applications β the parent guide on instruction ordering and layer reuse.
- Reducing Docker Image Size for Frontend Containers β pairing caching with smaller images.
- Securing and Invalidating Build Caches β key correctness that applies to Docker layers too.
- Build Optimization & Caching Strategies β the section overview.
β Back to Docker Layer Caching for Full-Stack Applications