The default docker build has no way to pass secrets without
exposing them in layers. BuildKit fixes this with RUN --mount
and RUN --ssh.
The fix matters because the alternative is not βslightly less
tidyβ. A credential that reaches an image layer or an image config
is in a content-addressed blob that you cannot edit, that has
already been replicated to every node that pulled it, and that no
amount of retagging or deleting will recall.
Build secrets (--mount=type=secret)
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \ npm ci
The secret file is mounted at /root/.npmrc (or wherever
specified) during the RUN step. After the step, the secret file
is gone. The layer produced by the step does not contain the
secret.
Multiple secrets:
RUN --mount=type=secret,id=aws_key \ --mount=type=secret,id=aws_secret \ ./deploy.sh
Every option, and the defaults that catch people
The Dockerfile reference documents these fields on
--mount=type=secret:
Field
Default
Notes
id
basename of target
The name the CLI must supply
target / dst / destination
/run/secrets/ + id
Where the file appears
env
β
Mount as an environment variable instead of a file (Dockerfile frontend 1.10.0+)
required
false
If false, a missing secret is silently empty
mode
0400
Octal file mode
uid
0
Owning user
gid
0
Owning group
On the CLI side, --secret takes id=<name>,src=<file> for a
file, id=<name>,env=<variable> to read from the environment, or
just id=<name> when the variable already has the same name.
Two of those defaults cause most of the support traffic.
Why an ARG-passed secret is in the image forever
Dockerβs build-variables documentation states it plainly: βBuild
arguments and environment variables are inappropriate for passing
secrets to your build, because theyβre exposed in the final image.β
The mechanism behind that sentence is what makes it non-negotiable.
Verifying secrets do not leak
Three checks, each of which can actually fail. Run them in CI, not
by hand after an incident.
Read-only / Safescan historyβ Search the image config history for credential-shaped strings. Non-zero exit means something was found.
The hostβs SSH agent is forwarded to the build container for the
duration of the step. After the step, the agent socket is gone.
The documented defaults: id is default, the socket appears at
/run/buildkit/ssh_agent.${N}, mode is 0600, and required
is false β so the same silent-failure warning applies. The N is
the index of the mount within the instruction, which is why you
rarely reference the path directly; BuildKit sets SSH_AUTH_SOCK
for you.
Common pitfalls
Putting secrets in ENV or ARG. Visible in docker history --no-trunc and in the image metadata. Do not do this.
Putting secrets in a COPY from the build context. The
secret file ends up in the layer. Do not do this.
Using a non-BuildKit builder. Legacy builders do not support
secret mounts; secrets must be passed via ARG (visible) or
via mount at runtime (does not help build).
Multi-stage with secrets. Secrets are scoped to the stage
that uses them. They are not visible to subsequent stages by
default. This is the correct behaviour; pass the secret
explicitly to each stage.
Assuming rotation invalidates the build. It does not β see
below.
Knowledge check
Knowledge check Β· 6 questions
Q1. To use a private Git repository at build time, the recommended BuildKit pattern is:
Q2. A `--mount=type=secret` value is persisted in the image's filesystem layers.
Q3. A token was passed with `--build-arg` six months ago. The tag has since been overwritten. What is the correct first action?
Q4. A stage runs `USER node`, then `RUN --mount=type=secret,id=npmrc,target=/home/node/.npmrc npm ci`, and the step fails with EACCES on that path. Why?
Q5. Which of these can expose a build-time credential to anyone who can pull the image? Select all that apply.
Q6. By default a forgotten `--secret` flag produces an empty mount rather than a build failure, so `required=true` is what makes the build fail.
Passing score: 75%. Answers are checked in this browser.