IAnswerable

Speed Up Docker Builds with Cache Mounts

24 Sep 2026

Every docker build reinstalling your entire node_modules or pip cache from scratch is wasted time. Cache mounts keep the package manager’s own cache directory between builds, without that cache ending up in the final image. A rebuild after a small code change goes from minutes to seconds.

Why layer caching alone doesn’t fix this

Docker normally caches layers by instruction, so RUN npm install only reruns if the layer above it (usually COPY package.json) changed. That works — until you touch package.json for any reason, at which point the whole install reruns from zero with an empty cache, because each build starts the cache directory fresh inside that layer.

Cache mounts sidestep that. Instead of caching the layer, you cache a directory that persists across builds regardless of what layer invalidated.

Using it

You need BuildKit, which is the default builder in current Docker installs. Confirm:

docker buildx version

Add the syntax directive and a cache mount to your Dockerfile:

# syntax=docker/dockerfile:1
FROM node:20

WORKDIR /app
COPY package.json package-lock.json ./

RUN --mount=type=cache,target=/root/.npm \
    npm ci

COPY . .
RUN npm run build

/root/.npm is npm’s cache directory, not node_modules itself — npm ci still writes node_modules normally, into the layer, so it ends up in the image. Only the download/extraction cache is reused between builds.

For pip:

RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt

For Go modules:

RUN --mount=type=cache,target=/root/.cache/go-build \
    --mount=type=cache,target=/go/pkg/mod \
    go build ./...

Each of these mounts a persistent cache directory that BuildKit keeps on the host (or in a buildx builder’s storage) across docker build invocations, even when the layer above them changed.

Confirm it’s working

Run the build twice and compare the timing of the cached step:

time docker build -t myapp .

First build populates the cache. Second build, with a trivial source change but unchanged dependencies, should show the install step taking a fraction of the original time — you’ll see CACHED for unrelated steps and a fast pass through the mount-cached one.

What breaks

If you’re building on CI with ephemeral runners, the cache mount doesn’t persist between separate runner instances by default — each fresh VM starts with no cache, same as before. You need --cache-from/--cache-to with a registry or buildx’s cache export to actually share it across CI runs, which is a separate mechanism from the local cache mount.

If you switch builders (docker buildx create) or clear BuildKit’s cache (docker builder prune), the cache mount is gone too — that’s expected, it’s a cache, not a volume, and nothing should depend on it existing.

Cache mount vs image size

The cache mount contents never land in the final image — that’s the whole point, and it’s different from a naive RUN npm install && rm -rf ~/.npm trick, which still bloats intermediate layers even if the final layer is smaller. --mount=type=cache never writes the cache to any layer at all.

The one gotcha worth knowing

Cache mounts are per-builder, and by default writes to them aren’t shared across concurrent builds of the same Dockerfile — BuildKit locks the mount per build to avoid corruption, so parallel CI builds of the same image can serialize on that lock instead of running fully in parallel. Add sharing=locked explicitly if you want that behavior guaranteed, or sharing=private if you’d rather each build get its own copy than wait.