BuildKit Cache Mounts: Fast Package Installs in Docker Builds
RUN --mount=type=cache gives Docker builds a persistent, shared cache across builds — pip, npm, apt, Go modules, and Cargo all get dramatically faster with it.
Published on • October 28, 2026
AI Assistant

If your Docker build spends four minutes re-downloading node_modules because you changed one source file, you’re hitting the limits of layer caching — and the fix isn’t a cleverer COPY order. It’s BuildKit’s cache mount: RUN --mount=type=cache, which gives build steps a persistent scratch directory that survives across builds, shared across builds on the same machine, and invisible in the final image.
Why layer caching isn’t enough
Docker’s classic caching model: each Dockerfile instruction is a layer, cached by hashing its inputs. COPY package.json then RUN npm install works well — until anything in package.json changes. The install layer is invalidated, and npm re-downloads every package from the network, even though 99% of the tarballs are identical to last build. The registry cache (--cache-from) helps across machines but not for this problem: the layer genuinely changed, so there’s no cache hit to reuse.
What you actually want is a cache keyed not on the Dockerfile instruction, but on the dependency set — and that’s precisely what a cache mount provides. It decouples “the cacheable data” (downloaded packages) from “the layer” (the instruction that produced it).
The mechanics
# syntax=docker/dockerfile:1
FROM node:22-slim
WORKDIR /app
# Mount npm's cache dir into this RUN only
RUN --mount=type=cache,target=/root/.npm \
npm ci --no-audit
COPY . .
RUN npm run build
Three properties make this work:
- The mount exists only during that
RUN. It’s not part of any layer, so it never bloats the image, and it doesn’t invalidate caching the wayCOPYing a cache directory in would. - It persists between builds. The cache directory lives in BuildKit’s internal storage, keyed by the mount’s
id(defaults to the target path). The next build of any Dockerfile on that daemon with the sameidstarts warm. - It’s safe for concurrency. Builds sharing a cache mount are serialized on that mount, so two parallel builds won’t corrupt each other’s
npmcache.
Adapting it per package manager
Each tool needs its cache directory mounted — the trick is just the cache, not the whole home dir:
| Tool | target= | Extra flags |
|---|---|---|
| npm | /root/.npm | — |
| pnpm | /root/.local/share/pnpm/store | pnpm config set store-dir first |
| pip | /root/.cache/pip | — |
| Go modules | /go/pkg/mod | often GOPATH=/go too |
| Cargo | /usr/local/cargo/registry | set CARGO_HOME accordingly |
| apt | /var/lib/apt/lists + /var/cache/apt | clean lists in same RUN |
For pnpm and Cargo, the cache directory the tool expects may differ from where you mount — set the tool’s env var (pnpm store dir, CARGO_HOME) to point at your mount target rather than fighting it.
apt deserves a mention: mounting /var/lib/apt/lists makes apt-get update nearly free (the metadata download was most of the cost), but the packages still download unless you also cache /var/cache/apt. Remember rm -rf /var/lib/apt/lists/* at the end of the same RUN — with the mount, it no longer costs you anything.
Beyond packages: monorepos and compiler caches
Cache mounts shine anywhere expensive, reusable state passes through a build step:
- Monorepo partial builds — mount the build system’s incremental directory (e.g., Turborepo’s
.turbo, Nx’s cache) so a changed package doesn’t rebuild its untouched siblings. - Language compiler caches — Go build cache, Rust
target/(carefully), ccache for C/C++. - Recurring downloads — Terraform providers, Chrome for Playwright/Puppeteer installs.
Debugging and hygiene
If cache mounts seem to do nothing, check the obvious first: the # syntax=docker/dockerfile:1 line (or BuildKit enabled via DOCKER_BUILDKIT=1) — without BuildKit, the instruction fails outright. On BuildKit, docker buildx du shows cache usage; if it grows unboundedly, scope mounts with an explicit id= per project and prune periodically.
One security note: cache mounts persist between builds, so a compromised build could plant content that a later build reads. For untrusted Dockerfiles, run builds with isolated cache (--no-cache-filter or dedicated builders) rather than sharing the daemon-wide cache.
The bottom line
Layer caching answers “did this instruction change?”; cache mounts answer “have I downloaded this package before?” — two different questions that happen to look identical in a Dockerfile. RUN --mount=type=cache gives package managers (and compilers, and incremental builders) a persistent, shared, out-of-image scratch directory, turning dependency-heavy builds from minutes into seconds. It’s the single highest-leverage line you can add to most Dockerfiles.