Chapter 5: Building Well
Topic 30

Cache Mounts and Build Secrets

CachingSecrets

Two BuildKit-only RUN --mount modes fix the two worst build-time habits. --mount=type=cache gives a RUN step a persistent directory — pip's wheel cache, apt's package cache — that survives across builds without ever becoming a layer, so dependency installs stop re-downloading the world. --mount=type=secret exposes a file, the Postgres password, to one RUN step at build time and to nothing else.

The secret never lands in a layer, never appears in docker history, and never becomes an ENV — which is exactly the fix for the ARG-baked-secret footgun from Chapter 4 topic 25. Both mounts depend on the # syntax line and on BuildKit being the active builder (topic 27); on a current Docker, they already are.

--mount=type=cache — Persistent Build Caches

RUN --mount=type=cache,target=/root/.cache/pip pip install … mounts a directory that BuildKit keeps between builds. The downloaded wheels persist outside the image, so the next build reuses them instead of re-fetching, and none of it becomes a shipped layer. The cache lives in BuildKit's own storage, attached to the step only while it runs and detached the instant it finishes.

A pip install backed by a persistent cache mount — nothing cached ships
# syntax=docker/dockerfile:1
FROM python:3.12 AS builder
WORKDIR /app
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
    pip wheel --wheel-dir /app/wheels -r requirements.txt

The /root/.cache/pip directory exists only during this step and persists in BuildKit between builds; it is not part of the image's filesystem. A second build with an unchanged requirements.txt finds the wheels already downloaded and skips the network entirely.

apt and Package Caches

The same mount on /var/lib/apt/lists and /var/cache/apt lets apt-get reuse package indexes across builds. This replaces the old "always rm -rf /var/lib/apt/lists in the same RUN" dance, because the cache now lives in the mount, not the layer — there is nothing to clean up in the image, since nothing was ever written to it.

--mount=type=secret — Build-Time Secrets

RUN --mount=type=secret,id=db_password … exposes the secret as a file — by default /run/secrets/db_password — for that one step only. You pass it from outside with docker build --secret id=db_password,src=./db_password.txt, and the value is gone the instant the step finishes: no layer, no history, no ENV.

A build step that reads the database password from a secret mount
# syntax=docker/dockerfile:1
FROM python:3.12 AS builder
WORKDIR /app
COPY . .
RUN --mount=type=secret,id=db_password \
    DB_PASSWORD="$(cat /run/secrets/db_password)" python manage.py migrate

You build it with docker buildx build --secret id=db_password,src=./db_password.txt .. The password is readable at /run/secrets/db_password while the migration runs and nowhere afterward — the mount is torn down with the step, leaving no trace in the layer or in docker history.

Why This Replaces the ARG Footgun

Chapter 4 topic 25 showed a password passed via ARG surviving in docker history and the layer cache for anyone to read. The secret mount makes the value available only inside the running RUN step and writes nothing persistent — which is the correct fix, not a workaround. An ARG is a build-time variable that Docker records; a secret mount is a file that exists only for one step and is never recorded at all.

Two BuildKit mount modes, two different jobs
--mount=type=cache
Mounts a directory — pip's wheel cache, apt's package cache — that persists across builds in BuildKit's own storage. It speeds repeat installs and never becomes a layer in the image.
--mount=type=secret
Exposes a file, the database password, to one RUN step at build time. The value lands in no layer and never appears in docker history — torn down the instant the step ends.

The Driftwood Build, Secured

Driftwood's builder stage needs the database password only to run a migration step at build time. It reads it via --mount=type=secret,id=db_password, uses a --mount=type=cache for pip so repeat builds skip re-downloading wheels, and the final image's docker history shows neither the password nor the cache. The password is supplied as a build secret, exactly as the chapter promised.

Runtime secrets — the password the running container needs to actually connect to Postgres — are a separate mechanism handled in Chapter 6 and Chapter 10. A build secret solves the build-time leak; it does not feed a long-lived process.

Build Secret vs Build ARG

Build ARG — a build-time variable Docker records. Its value is visible in docker history, persists in the layer cache, and is trivially extractable from the image. Use it for non-sensitive build-time configuration: a version number, a feature flag, a mirror URL. Never put a credential in one.

Build secret (--mount=type=secret) — a file mounted for a single RUN step that leaves nothing in any layer or in history. It is the only correct way to give a credential to the build. Use it for anything you would be unhappy to publish — the database password, an API token, a private registry credential.

Common Mistakes
  • Passing the database password via --build-arg DB_PASSWORD=… and trusting it is gone — docker history shows the value and it sits in the build cache; this is the exact Chapter 4 footgun, unfixed.
  • Echoing a mounted secret into a file or ENV inside the RUN step "to use it later" — that write becomes a layer and re-leaks the secret the mount was protecting.
  • Putting a cache mount's contents on the critical path for correctness — a cache mount is best-effort and can be empty or pruned, so the build must still succeed (just slower) when the cache is cold.
  • Combining a cache mount with an in-layer rm of the same cache directory — the rm does nothing useful because the cache was never in the layer, and you may be deleting the mount you wanted to keep warm.
  • Forgetting the # syntax=docker/dockerfile:1 line and writing --mount=type=secret — it fails to parse, because these mounts are BuildKit frontend features (topic 27).
Best Practices
  • Deliver every build-time credential through --mount=type=secret, never through ARG or ENV, so nothing sensitive reaches a layer or docker history.
  • Add --mount=type=cache to pip and apt install steps, so dependency downloads persist across builds without bloating the image.
  • Keep cache mounts strictly an optimization — write the RUN so it still produces a correct image when the cache is empty.
  • Pass secrets from a file at build time (--secret id=…,src=…) and keep that source file out of the build context via .dockerignore, so it never enters the image by either path.
Comparable tools buildx · BuildKit the only Docker builder with these RUN --mount modes Kaniko its own caching and a different secret-handling approach for in-cluster builds Podman · Buildah supports --secret build secrets with compatible syntax Buildpacks · ko manage dependency caching internally, no exposed RUN --mount

Knowledge Check

What does --mount=type=cache persist, and why does it never become a layer?

  • A directory BuildKit keeps between builds — pip wheels, apt indexes — that lives outside the image, so it ships in no layer
  • A layer that BuildKit automatically detects and deletes from the image after the build finishes
  • The application's runtime data directory, mounted into the build so user uploads and database rows survive container restarts and image rebuilds
  • The build secret value, so later builds can reuse the same stored credential

How does --mount=type=secret expose a value, and what does it leave behind?

  • As a file readable by one RUN step, torn down when the step ends — leaving nothing in any layer or in docker history
  • As an environment variable injected at the top of the build and available to every subsequent RUN and COPY step until the build finishes
  • As a file copied into a persistent layer so later RUN steps can read it
  • As a value recorded in docker history for later audit purposes

Why is a build secret the correct fix for the ARG-in-history leak?

  • An ARG is recorded in history and the cache; a secret mount writes nothing persistent, so the value is never exposed
  • A build secret runs under BuildKit while an ARG can only run under the legacy builder
  • The build secret is encrypted inside the layer with a per-build key and decrypted on pull, while the ARG is stored there in plaintext for anyone to read
  • A build secret is automatically available to the running container at runtime too

Why do these mounts require BuildKit and the # syntax line?

  • They are BuildKit frontend features; without the # syntax line an older daemon can't parse --mount, and the legacy builder has no such instruction
  • The # syntax line pins the base image digest that the cache and secret mounts depend on, so the mounted paths resolve to the same files on every rebuild across machines
  • BuildKit needs the line to encrypt the cache and secret directories on disk
  • Setting DOCKER_BUILDKIT=0 in the environment is what enables the mount instructions

You got correct