What Are Build Cache Mounts?
The BuildKit feature that lets a package manager remember its downloads between builds — without ever letting those downloads bloat the final image.
Think about how a professional kitchen stores its pantry staples. Flour, sugar, and oil sit in labeled bins on a shelf, refilled as needed, ready for the next dish — the chef never has to walk to the store and buy a fresh bag of flour every single time a recipe calls for a pinch of it. Now imagine a much stranger kitchen where every single dish starts by ordering a brand-new bag of flour, using one tablespoon, and throwing the rest away. That second kitchen is how container builds worked for years: every build re-downloaded every package dependency from scratch, even when nothing about those dependencies had changed since yesterday. Build cache mounts are the shelf of labeled bins — a place for a build step to keep its own working supplies between runs, without those supplies ever ending up in the final dish.
WWhy Build Cache Mounts Had to Exist
A container image is built from layers, and every layer’s final content becomes part of the shipped artifact. Historically, this created an awkward tension. If a RUN npm install or RUN pip install step downloaded packages into a cache directory to speed up future installs, that cache directory became part of the layer — permanently baked into the image, adding tens or hundreds of megabytes that no running application ever actually needed. The common workaround was to explicitly clear the package manager’s cache at the end of the same RUN instruction, which kept the image lean but meant every single build, even an unrelated one on the same machine, had to re-download everything from the internet all over again.
Imagine you could either (a) keep every grocery receipt and food scrap from tonight’s dinner glued permanently onto your dining table, or (b) throw everything away completely after every meal, including things you will obviously need again tomorrow. Neither option is good. What you actually want is a pantry: supplies that persist between meals but never show up on the table itself. A build cache mount is exactly that pantry for a build step.
BuildKit — the graph-based build engine that became Docker’s default builder — solved this with a dedicated mount type built specifically for this purpose: RUN --mount=type=cache,target=/some/path. This tells BuildKit to attach a persistent, reusable directory to that specific point in the container’s filesystem for the duration of the command only, then detach it again before the resulting layer is captured — so the directory’s contents genuinely persist across builds, while never being written into any layer at all.
A cache mount is not the same thing as BuildKit’s general step-level result caching (which skips re-running an unchanged command entirely). A cache mount is a persistent scratch directory available inside a command’s execution, used for things like package manager download caches — it helps even when the command itself must run, because the command’s own internal cache is still warm.
CCore Concepts You Need First
Cache Mount
A BuildKit mount type that attaches a persistent directory to a RUN instruction, surviving across separate build invocations, without becoming part of any image layer.
Cache ID
An optional label identifying which cache a mount should use, allowing multiple RUN instructions, or even multiple images, to deliberately share one cache directory.
Sharing Mode
Controls how concurrent builds using the same cache ID coordinate access: shared, private, or locked.
Cache Mount vs. Layer
A layer is a permanent, shipped part of the image. A cache mount’s directory is entirely separate storage, attached only while the command runs, then unmounted and discarded from the layer’s perspective.
Package Manager Cache
The directory a tool like npm, pip, apt, or Go’s module system uses to store previously downloaded packages, so it can skip re-downloading them later.
Build Graph Node
In BuildKit’s internal dependency graph (LLB), the single RUN instruction that declares a cache mount, distinct from the persistent cache storage itself, which lives outside the graph.
AArchitecture and Components
graph TB
A["Dockerfile RUN instruction\nwith --mount=type=cache"] --> B["BuildKit Solver"]
B --> C["Cache Mount Manager"]
C -->|"Looks up cache id"| D["Named Cache Volume\n(persists on build host)"]
D -->|"Attached read-write"| E["Executor\n(runs the command)"]
E -->|"Command finishes"| F["Cache directory detached"]
E -->|"Only non-cache filesystem changes"| G["Snapshotter\n(captures the real layer)"]
G --> H["Final Image Layer\n(cache contents excluded)"]
D -->|"Reused next build"| C
The Named Cache Volume
Behind the scenes, BuildKit maintains its own pool of persistent storage volumes on the build host (or build worker, in distributed setups), completely separate from the ordinary image layer cache. Each of these volumes is identified by a cache ID — either one explicitly given by the Dockerfile author, or an automatically derived default tied to the mount’s target path. This is the actual “pantry shelf” that survives from one build to the next.
The Cache Mount Manager
When the solver reaches a graph node declaring a cache mount, a dedicated manager component resolves the cache ID to its named volume, attaches that volume at the requested target path inside the executor’s isolated filesystem, and enforces whichever sharing mode was requested if multiple builds try to use the same cache concurrently.
The Snapshotter’s Blind Spot (By Design)
Once the command finishes, BuildKit detaches the cache mount before asking the snapshotter to record what changed in the container’s real filesystem. Because the cache directory was never part of that real filesystem tree from the snapshotter’s point of view — it was a separate mount stacked temporarily on top — nothing inside it is captured into the resulting layer. This is the specific architectural trick that keeps caches fast without keeping them in the shipped image.
IInternal Working — Sharing Modes and Isolation
Cache mounts support three sharing modes, and choosing the right one matters once more than one build might use the same cache at the same time — which happens constantly on a busy CI cluster building several branches in parallel.
Shared (the default)
Multiple concurrent build steps can read and write the same cache volume at the same time, with no locking. This works well for tools designed to tolerate concurrent access to their own cache directory, such as many package managers’ download caches, where two processes writing different package files rarely conflict.
Private
Each concurrent build gets its own separate, temporary copy of the cache rather than sharing one volume live. This trades away some cross-build reuse for guaranteed isolation, useful when a tool’s cache format is not safe under concurrent writes.
Locked
Concurrent builds needing the same cache ID queue up and take turns, one at a time, guaranteeing exclusive access without the memory overhead of fully copying the cache per build. This suits caches that are large, expensive to duplicate, and used by tools that would corrupt their own state under true concurrent writes.
Shared mode is like a communal office printer everyone uses freely, because print jobs rarely truly collide. Private mode is like giving each employee their own personal printer, guaranteeing no interference but costing more overall. Locked mode is like one shared printer with a sign-up sheet — everyone eventually gets access, but only one job runs at a time.
“If a cache mount isn’t part of the final layer, how can BuildKit still keep the build reproducible?” A strong answer: reproducibility of the final image depends on the command’s real filesystem effects, not on which internal cache accelerated it — an npm install with a warm cache and one with a cold cache should still install the exact same package versions if the lockfile is respected, so the cache mount speeds up the process without changing its correctness guarantees.
Internally, the cache ID is also part of what feeds into BuildKit’s broader content-addressable caching model, but it is tracked and stored as its own distinct namespace of volumes, separate from the layer-result cache discussed in general BuildKit architecture — the two caching systems solve different problems and are easy to conflate at first.
DData Flow and Lifecycle of a Cache Mount
Dockerfile Declares the Mount
A RUN instruction specifies –mount=type=cache, a target path, and optionally an explicit cache id and sharing mode.
Solver Resolves the Cache Volume
Before executing, BuildKit’s cache mount manager finds or creates the named persistent volume associated with that cache id, honoring the sharing mode’s coordination rules.
Executor Attaches the Volume
The volume is mounted read-write at the requested target path inside the isolated build environment, alongside the regular root filesystem from earlier build steps.
Command Runs Using the Warm Cache
The package manager or build tool inside the container reads and writes its cache directory as normal, unaware anything unusual is happening underneath it.
Mount Detaches Before Layer Capture
When the command finishes, BuildKit unmounts the cache volume before the snapshotter records the resulting filesystem diff, so cache contents never appear in that diff.
Volume Persists for the Next Build
The named cache volume remains on the build host, ready to be reattached the next time any build step references the same cache id — even for a completely different image.
AAdvantages, Disadvantages and Trade-offs
Advantages
- Dramatically faster dependency installation on repeated builds, without adding a single byte to the final image
- Cache can be shared across entirely different images, not just repeated builds of the same one, when a cache id is chosen deliberately
- Removes the old trade-off between build speed and image size, which previously forced clearing caches inside the same layer
- Sharing modes give explicit control over safety versus speed for concurrent builds
- Works transparently with existing package manager tooling, requiring no changes to the tool itself
Disadvantages / Trade-offs
- Cache mounts live on the build host, not in the portable image, so a fresh CI runner with no prior cache starts cold every time unless a remote cache strategy is also configured
- Because cache contents are excluded from the layer, builds are not perfectly reproducible from cache state alone — reproducibility still depends on lockfiles and pinned versions
- An unexpectedly large or corrupted cache volume can silently consume build host disk space over time if never pruned
- Shared mode’s lack of locking can occasionally cause subtle races for tools whose own cache format is not safe under concurrent access
SSecurity
Because cache mount contents are explicitly excluded from image layers, they cannot accidentally leak into a shipped image the way a forgotten package manager cache directory once could — this closes a minor but real historical source of images unintentionally including stale, unreviewed dependency archives.
However, a cache volume is still a long-lived piece of state sitting on the build host or build cluster, and it deserves its own security thinking. A cache shared across many different projects (via a common cache id) means a compromised or malicious build step could in principle poison a cache directory that a completely different, later build then trusts implicitly. Using distinct, narrowly scoped cache ids per project, and periodically pruning caches, limits the blast radius of this kind of cross-build contamination.
Cache mounts are not a substitute for the dedicated secret-mount feature. A credential written into a cache mount’s directory can persist across builds and even be readable by unrelated builds sharing the same cache id — secrets belong in --mount=type=secret, never in a cache mount’s storage.
MMonitoring, Logging and Metrics
BuildKit reports cache mount activity as part of its normal structured build progress output, and buildx’s du and prune subcommands surface how much disk space named caches are consuming on a given build host, letting platform teams monitor cache growth over time rather than discovering a full disk unexpectedly during a build.
On centralized, shared build clusters, buildkitd’s Prometheus-compatible metrics endpoint can be scraped to track cache hit effectiveness over time, giving platform teams a way to see whether cache mounts are genuinely paying off in aggregate build-time reduction across every team relying on the shared builder.
DDesign Patterns and Anti-patterns
Pattern: One Cache ID Per Package Ecosystem
Giving each language ecosystem’s cache directory (npm, pip, Go modules, Maven) its own explicit, descriptive cache id keeps caches cleanly separated, makes disk usage easy to reason about per tool, and avoids one large tool’s cache crowding out another’s under shared storage limits.
Pattern: Locked Mode for Large, Fragile Caches
For a cache directory belonging to a tool known to misbehave under concurrent writes — some compiler or dependency-resolution caches fall into this category — deliberately choosing locked sharing mode trades a small amount of build queuing time for guaranteed correctness.
Anti-pattern: Treating a Cache Mount as Guaranteed to Exist
Writing a Dockerfile that assumes the cache mount will always contain something useful breaks on a brand-new build host with an empty cache, or a CI runner that never persists build host state between jobs. Correct usage always tolerates a cold, empty cache gracefully — cache mounts are an optimization, not a dependency.
Anti-pattern: One Giant Shared Cache Id for Everything
Reusing a single, generic cache id across every project and every tool defeats the purpose of scoping — unrelated projects end up fighting over the same volume, and pruning or debugging one project’s cache growth becomes impossible to isolate from another’s.
BBest Practices and Common Mistakes
Name every cache id explicitly
Do not rely solely on the default id derived from the target path — explicit ids make intent and sharing scope clear to anyone reading the Dockerfile later.
Match sharing mode to the tool’s concurrency safety
Default to shared for well-behaved caches, and move to locked only for tools known to need exclusive access.
Prune caches on a schedule
Run buildx prune regularly on long-lived build hosts and shared CI build clusters to prevent unbounded disk growth.
Keep the package manager’s normal cache path as the target
Point the mount at the tool’s actual default cache directory rather than a custom path, so the tool’s built-in cache logic works unmodified.
Confusing cache mounts with layer caching
A cache mount speeds up what happens inside a command; it does not, by itself, skip re-running that command the way BuildKit’s step-result cache does.
Expecting cache mounts to transfer between build hosts automatically
Without an explicit remote or distributed cache strategy, a cache volume lives only on the specific host or worker that created it.
RReal-World and Industry Examples
Node.js Projects and npm/yarn Caches
Large JavaScript monorepos commonly mount npm’s or yarn’s cache directory as a persistent cache mount, cutting repeated dependency installation time from minutes to seconds on CI runners that reuse the same build host between jobs.
Go Module and Build Caches
Go’s own module download cache and its compiler build cache are frequently mounted this way in Dockerfiles building Go services, since Go’s toolchain is specifically designed to work well with a persistent, incrementally warmed cache directory across builds.
Debian and Ubuntu Package Caches (apt)
Teams building Debian-based images commonly cache /var/cache/apt using a cache mount, avoiding repeated downloads of the same system packages across many related images built from a shared base, while still keeping the final image’s layer free of the package archive files.
Shared CI Build Clusters
Organizations running centralized BuildKit-based build clusters — rather than ephemeral, always-cold CI runners — get the largest practical benefit from cache mounts, since the same named cache volumes stay warm across many different teams’ builds throughout the day.
FFrequently Asked Questions
SSummary and Key Takeaways
Key Takeaways
- Build cache mounts solve a real tension between fast, incremental package installs and small, clean final images — without forcing a choice between them.
- The mechanism works by attaching persistent storage only during command execution, then detaching it before the snapshotter captures the resulting layer.
- Sharing modes — shared, private, and locked — give explicit control over how concurrent builds coordinate access to the same cache.
- A cache mount lives on the build host, not in the image, so its benefit depends on build infrastructure that keeps that host warm between builds.
- Cache mounts and secret mounts solve different problems and should never be confused — credentials belong exclusively in secret mounts.
- Deliberate, scoped cache ids per project and per tool keep caches easy to reason about, monitor, and prune safely.
- The single biggest practical win shows up in package-manager-heavy builds — npm, pip, Go modules, apt — run repeatedly on a persistent build host or shared CI cluster.