How does BuildKit's `RUN --mount=type=secret` differ from passing a value with `--build-arg`, what exactly does the build step see, and what still leaks if you use it carelessly?
answer
- tmpfs file for one RUN only
- default target /run/secrets/<id>
- required=true or it silently proceeds
- not in layer diff, not in history
- secret content is not in the cache key
basics
~20 sA secret mount exposes the value as a temporary in-memory file, mounted only for that one RUN instruction. It is not written to any layer and its value never appears in the image history. It still leaks if the command copies it into the filesystem or prints it.
solid answer
~50 sWith BuildKit you declare `RUN --mount=type=secret,id=npmrc,target=/root/.npmrc,required=true npm ci` and supply the value at build time with `--secret id=npmrc,src=...` or `--secret id=token,env=TOKEN`. During that single instruction the value appears as a file on a tmpfs mount (default path `/run/secrets/<id>`, with `mode`, `uid`, `gid` options). When the instruction finishes, the mount is gone. The resulting layer contains only what the command produced, and the image history records the *mount flag*, never the value, so `docker history` and `docker inspect` show nothing. Contrast that with `--build-arg`, whose value is baked into the history entry. Careless use still leaks: if the command copies the secret into the image, or echoes it into a log that CI or an exported cache retains, it escapes. One more gotcha — secret contents are not part of the layer cache key, so changing the secret does not invalidate a cached step.
code
dockerfile · 8 lines# syntax=docker/dockerfile:1
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc,required=true \
npm ci --omit=dev
COPY . .
CMD ["node", "server.js"]go deeper
Know the syntax and the headline: the value is a temporary file for one instruction and never enters the image.
Explain the mount options, the default /run/secrets path, why it is absent from the layer diff and history, and the SSH mount variant.
Add the operational caveats: cache-key exclusion, sensitive exported caches, non-root permission settings, and enforcing required=true in review.
Position it as the delivery mechanism inside a broader rule that long-lived credentials never reach builders in the first place.
## What BuildKit is BuildKit is the modern build backend for Docker (the default since Engine 23.0, and always used by `docker buildx`). It builds a DAG of build steps instead of a linear script, which is what allows an instruction to have *mounts* whose lifetime is only that instruction. Dockerfiles using these features should begin with the syntax directive `# syntax=docker/dockerfile:1` so the supporting frontend is fetched. ## The mechanism ``` RUN --mount=type=secret,id=npmrc,target=/root/.npmrc,required=true \ npm ci ``` BuildKit mounts the supplied value as a tmpfs-backed file inside the step's container. Options: `id` (the name matched on the CLI), `target`/`dst` (default `/run/secrets/<id>`), `required=true` (fail the build if not provided — use it, otherwise a missing secret silently produces a broken or unauthenticated build), and `mode`/`uid`/`gid` for permissions when the step runs as a non-root user. On the CLI you supply it from a file, `--secret id=npmrc,src=$HOME/.npmrc`, or from the environment, `--secret id=token,env=GITHUB_TOKEN`. In a Compose or Bake definition it is declared as a build secret rather than an argument. ## Why it does not leak Two properties matter. The mount is **not part of the layer**: the step's filesystem diff excludes mounted paths, so the file cannot be captured in the layer tarball. And the **history entry** records the instruction with its mount flag, not the secret's value — the value never enters image metadata, unlike `ARG`, which is recorded in the expanded command line. Nothing is left for `docker history`, `docker inspect`, or an unpacked `docker save` to reveal. There is a sibling mount for SSH: `RUN --mount=type=ssh git clone git@host:repo` forwards your ssh-agent socket into the step, so private-repository access needs no key material in the image at all. ## What still goes wrong - **The command copies it.** `RUN --mount=type=secret,id=k cp /run/secrets/k /app/key` writes the value into the layer. The mount protects delivery, not what you do with it. - **The command prints it.** Anything echoed lands in build logs, which CI systems retain and often expose broadly. - **Cache semantics.** The secret's *content* is not part of the cache key, so a cached step is reused even after the secret changes. That is usually fine, but do not rely on "the build will fail if the token is stale"; conversely, exported or remote caches can retain artifacts produced with the secret, so treat cache backends as sensitive storage. - **Non-root builds.** If the step runs as an unprivileged user, a default mode of 0400 owned by root makes the file unreadable; set `uid` and `mode` explicitly. - **Legacy builder.** With BuildKit disabled (`DOCKER_BUILDKIT=0`) the syntax is unsupported and the build fails — worth knowing when older CI images are involved. ## How to frame it in an interview Say what it replaces (`ARG`/`ENV` for credentials), what the step sees (a tmpfs file for the duration of one instruction), why it is invisible afterwards (not in the layer diff, not in the history), and then volunteer the caveat that it protects delivery rather than misuse. That last point separates someone who has used it from someone who has read about it.
- You added a secret mount but the build step fails with a permission error reading /run/secrets/token. What is happening?The mounted file defaults to root ownership with restrictive permissions, and your step runs as a non-root USER. Set uid, gid or mode on the mount, for example --mount=type=secret,id=token,uid=1000,mode=0400, so the build user can read it. Do not fix it by switching the step back to root just to read the file.
- Does using a secret mount mean a rotated credential automatically invalidates cached layers?No. Secret contents are deliberately excluded from the layer cache key, so a step cached from a previous build is reused even when the secret value changes. That is usually desirable, but it means you cannot rely on cache invalidation to notice a rotation; force a rebuild explicitly when that matters.
saying these in an interview costs you the question
- Thinking the mount protects a secret the command itself copies into the image
- Omitting required=true and shipping a build that silently proceeded without the secret
- Assuming it works with the legacy builder or without the dockerfile:1 syntax directive
- Claiming the secret value appears in docker history — only the mount flag does
- Echoing the secret to stdout for debugging, into retained CI logs