What Are Build Cache Mounts?

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

Two very different problems — slow builds and bloated images — used to force developers to choose between them. Cache mounts let them have both solved at once.

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.

Everyday Analogy

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.

i
Key Distinction

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

Term

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.

Term

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.

Term

Sharing Mode

Controls how concurrent builds using the same cache ID coordinate access: shared, private, or locked.

Term

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.

Term

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.

Term

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

A cache mount sits at the intersection of three things BuildKit already manages separately: the build graph, the snapshotter, and a dedicated cache storage area kept apart from ordinary image layers.
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
    
Fig. 1 — A cache mount’s storage lives outside the layer path entirely, attached only during execution

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.

Everyday Analogy

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.

?
What an interviewer may ask

“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

1

Dockerfile Declares the Mount

A RUN instruction specifies –mount=type=cache, a target path, and optionally an explicit cache id and sharing mode.

2

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.

3

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.

4

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.

5

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.

6

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.

!
Common Pitfall

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.

buildx du
inspect cache disk usage
buildx prune
reclaim stale cache space
Prometheus
fleet-wide cache metrics via buildkitd

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

Best Practice

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.

Best Practice

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.

Best Practice

Prune caches on a schedule

Run buildx prune regularly on long-lived build hosts and shared CI build clusters to prevent unbounded disk growth.

Best Practice

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.

Common Mistake

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.

Common Mistake

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

Q1Do build cache mounts work with the classic Docker builder?
No. Cache mounts are a BuildKit-specific feature, exposed through the –mount flag on a RUN instruction. The classic, legacy builder has no equivalent mechanism, which was one of the concrete motivations for BuildKit’s design.
Q2Will using a cache mount make my final image smaller?
It prevents the cache directory’s contents from being added to the image in the first place, which typically results in a smaller image than the old pattern of caching and then manually deleting the cache within the same layer, since some residue from delete operations can still affect layer size in certain filesystem setups.
Q3Can two completely different Dockerfiles share the same cache?
Yes, as long as both explicitly specify the same cache id in their mount declarations. This is intentionally useful for organizations building many related images that share a common dependency ecosystem.
Q4Is a cache mount the same as a Docker volume used at container runtime?
No. A runtime volume is attached to a running container after the image is built and deployed. A build cache mount only exists during the build process itself, is managed entirely by BuildKit, and has no relationship to volumes used by the resulting running application.
Q5What happens to a cache mount’s data if the build host is destroyed?
The cache is lost along with the host, since it is local persistent storage tied to that specific buildkitd instance. Ephemeral CI runners that are recreated from scratch for every job will not see any benefit from cache mounts unless the organization sets up a persistent, shared build cluster or an external remote cache mechanism.

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.