open atlas
↑ Back to track
Docker, containers as a system DOCK · 05 · 03

Cache mounts and build secrets: --mount=type=cache keeps downloads warm, --mount=type=secret keeps tokens out of layers

RUN --mount=type=cache persists a package cache across builds even on cache-miss layers, turning a cold npm/go/pip download warm. RUN --mount=type=secret feeds a credential to one command without writing it to a layer — the fix for the token ARG and ENV bake into image history.

DOCK Senior ◷ 16 min
Level
FoundationsJuniorMiddleSenior

The rotation alert came from GitHub itself: a secret-scanning bot had found a live npm automation token in a public image on Docker Hub. The team’s first reaction was disbelief — the token was passed as a build arg, --build-arg NPM_TOKEN=..., and the Dockerfile deleted the .npmrc right after npm ci. They were sure it was gone. Then someone ran docker history --no-trunc on the published image and there it was, in plain text, in the metadata of the ARG NPM_TOKEN layer — and a second time in the RUN echo "//registry..._authToken=${NPM_TOKEN}" > .npmrc command string. Deleting the file in a later layer changed nothing: every layer is immutable and shipped, so the rm just added a layer that hides a file still present, byte for byte, in the layer below. The token had been baked into the image history of every build for three months. The fix was not a better rm. It was RUN --mount=type=secret, which hands the credential to exactly one command and never writes it to any layer at all.

Cache mounts: keep the package cache warm across builds

Here is the frustrating loop every engineer hits: you move package-lock.json before COPY . . to optimize ordering, but the lockfile still changes on every dependency bump and you are back to a three-minute npm ci. Good ordering minimizes reruns; it cannot eliminate them. Layer caching is all-or-nothing per vertex: change one byte the RUN npm ci depends on and the whole step reruns cold, re-downloading every dependency. But a package manager already has its own internal cache (npm’s ~/.npm, Go’s /go/pkg/mod, pip’s ~/.cache/pip, apt’s /var/cache/apt) — and normally that cache lives inside the layer, so a layer cache miss throws it away too. RUN --mount=type=cache breaks that coupling: it mounts a persistent BuildKit-managed directory into the build that survives across builds independently of layer caching.

# syntax=docker/dockerfile:1
FROM node:22 AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci

Now even when package-lock.json changes and the npm ci layer misses, the ~/.npm cache mount is still populated from prior builds, so npm re-uses already-downloaded tarballs and only fetches what is genuinely new. On a large monorepo this turns a ~3-minute cold npm ci into a ~25-second warm one; a Go service’s go mod download over a /go/pkg/mod cache mount drops from minutes to seconds. The cache mount is not in any layer — it never ships — and BuildKit serializes concurrent access with a sharing mode (default shared; use locked for tools that cannot tolerate concurrent writers, like some package databases).

Quiz

A RUN npm ci step uses --mount=type=cache,target=/root/.npm. A developer changes one line in package-lock.json and rebuilds, so the npm ci layer is a cache miss. What does the cache mount do for this rebuild?

Why this works

Why is a cache mount different from just ordering instructions well? Ordering (manifests before source) keeps a layer a cache hit when its inputs are unchanged — but the moment a lockfile legitimately changes, that layer must rerun, and without a cache mount it reruns fully cold. The cache mount addresses the orthogonal problem: making the unavoidable reruns cheap. The two compose. Good ordering minimizes how often the install reruns; a cache mount minimizes how expensive each rerun is. Skip the mount and every dependency bump pays the full cold-download tax; skip the ordering and you rerun far more often than necessary. Senior Dockerfiles do both, which is why their CI is fast even on dependency churn.

Build secrets: a credential that touches no layer

The Hook’s failure is structural, not a mistake of forgetting an rm. ARG and ENV values are recorded in the image config and docker history; any command string that interpolates a secret is recorded verbatim; and every layer is immutable, so a later rm cannot remove bytes from an earlier layer — it only stacks a whiteout on top while the data sits readable in the layer below, shipped to anyone who pulls. RUN --mount=type=secret solves it at the root: BuildKit mounts the secret as a file (default /run/secrets/<id>) only for the duration of that one RUN, and it is never written to the layer, the config, or the history.

# syntax=docker/dockerfile:1
FROM node:22
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=secret,id=npmtoken \
    NPM_TOKEN=$(cat /run/secrets/npmtoken) \
    npm ci
# build with:  docker build --secret id=npmtoken,env=NPM_TOKEN .

The token is read from the mounted file inside the command, used by npm ci, and gone when the step ends. docker history shows the RUN with the --mount flag and no token. The published image carries nothing. You provide the value at build time from an env var (--secret id=npmtoken,env=NPM_TOKEN) or a file (--secret id=npmtoken,src=./token) — never as a build arg. The same mechanism delivers SSH agents (--mount=type=ssh) for private git deps without baking a key. The rule that prevents the whole class of incident: a credential needed only at build time goes through a secret mount; it never appears in ARG, ENV, or a command string.

Quiz

A Dockerfile does ARG TOKEN, then RUN echo "token=$TOKEN" > .npmrc && npm ci && rm .npmrc. The final image deletes .npmrc. Is the token recoverable from the published image?

Recall before you leave
  1. 01
    Explain what RUN --mount=type=cache does and why it helps even when the layer is a cache miss.
  2. 02
    Why do ARG, ENV, and rm fail to keep a build secret out of an image, and how does --mount=type=secret fix it?
Recap

Two BuildKit mounts solve two recurring build problems that ordering alone cannot. RUN —mount=type=cache mounts a persistent, BuildKit-managed directory — npm’s ~/.npm, Go’s /go/pkg/mod, pip’s or apt’s cache — that lives outside the image layers and survives across builds independently of layer caching. Layer caching is all-or-nothing per step, so a single lockfile change forces RUN npm ci to rerun; normally that discards the package manager’s own download cache and the install reruns cold, but the cache mount keeps that cache intact across builds, so the rerun reuses prior tarballs and fetches only what changed — a ~3-minute cold install becomes ~25 seconds, a Go go mod download drops from minutes to seconds, and the cache never ships in a layer. It composes with good ordering: ordering minimizes how often the install reruns, the cache mount minimizes how expensive each rerun is. RUN —mount=type=secret addresses a structural leak, not a careless rm. ARG and ENV are recorded in the image config and docker history, interpolated command strings are recorded verbatim, and because layers are immutable and all ship, a later rm only stacks a whiteout while the secret bytes stay readable in the layer below — so a deleted .npmrc still leaks the token from the published image, three ways over. A secret mount hands the credential to exactly one RUN as a file under /run/secrets, used and gone when the step ends, written to no layer, config, or history, with the value supplied at build time via —secret id=…,env=… or src=… rather than a build arg. Now when you see a build arg carrying a token — or a three-minute install on every dependency bump — you know the mount to reach for: —mount=type=secret keeps credentials out of layers, —mount=type=cache keeps reruns fast.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 5 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.