Build secrets (BuildKit --secret)#
How to give a build a credential — a private-repo token, a registry login, a package-index password — without that credential landing in an image layer, in the build cache, or in the repository.
The repo runs an enforcing secret scan (secret-scan in
linux/scripts/preflight.sh, via linux/scripts/lint-secrets.sh), so a
credential committed by accident fails the gate. This page is the sanctioned way
to avoid needing one in the first place.
Why not the obvious approaches#
Approach |
What goes wrong |
|---|---|
|
Recorded in image metadata; |
|
Becomes a layer. Deleting it in a later layer does not remove it |
|
Persists into every derived image and every running container |
A BuildKit secret mount is none of these: the file is exposed only for the
duration of one RUN, on a tmpfs, and is never committed to a layer.
The pattern#
1. Put the credential in a file outside the build context. For a git host,
a .netrc is the least invasive form — git picks it up with no other config:
cat > .gitlab.netrc <<'NETRC'
machine git.example.com
login <username>
password <PERSONAL_ACCESS_TOKEN_OR_DEPLOY_TOKEN>
NETRC
Make sure this file is covered by .gitignore and
.dockerignore before you create it.
2. Pass it to the build by id, not by value:
docker build \
--secret id=gitlab_netrc,src=./.gitlab.netrc \
-t ghcr.io/kataglyphis/<image>:latest \
--push .
3. Mount it for exactly the step that needs it:
RUN --mount=type=secret,id=gitlab_netrc <<'EOF'
set -euo pipefail
# The secret is mounted read-only at /run/secrets/<id> by default.
cp /run/secrets/gitlab_netrc /root/.netrc
chmod 600 /root/.netrc
trap 'rm -f /root/.netrc' EXIT
git clone https://git.example.com/<org>/<repo>.git
uv pip install -v -e ./<repo>
EOF
Details that matter#
Default mount path is
/run/secrets/<id>. Override withtarget=/some/pathif a tool insists on a fixed location.The copy is what needs cleaning up, not the mount. The mount disappears when the
RUNends. Thecpto/root/.netrcdoes not — hence thetrap. Without it the credential is committed to that layer, which is exactly the failure the secret mount exists to prevent.Keep it to one
RUN. A secret mounted on step N is not visible on step N+1. Cloning and installing in the same heredoc is deliberate.set -euo pipefailinside the heredoc. Without it a failedgit clonestill exits 0 and the build continues with an empty directory — see the shell safety conventions inAGENTS.md.Requires BuildKit. Both
docker buildx buildandnerdctl builduse it; a legacyDOCKER_BUILDKIT=0build rejects--secret.
Windows: a secret from the environment (the sccache S3 key)#
The Windows lane’s one build secret is the key to sccache’s shared S3 bucket
(windows-build-resources.md § Two cache levels).
It never exists as a file, on either side of the build:
Where |
What holds the key |
|---|---|
The build host |
Two user environment variables, |
The build call |
|
The RUN |
|
The image |
Nothing. The publish gate fails an image environment carrying |
The server |
|
The repositories |
Placeholders only: |
On Windows, prefer the env= form over a file target. A file target works too,
but it needs a forward-slash path (target=C:/…) and it leaves a 0-byte
placeholder in the layer.
A secret pasted into a chat, an issue or a log has left the machine: rotate it.
On the server that is garage key create, then garage bucket allow, then
garage key delete on the old key (linux/sccache-s3/README.md).
The objects in the bucket stay valid. Then run Set-SccacheS3Level.ps1 again on each host.
Runtime secrets: Compose, not BuildKit#
Everything above is build-time. A secret a running container needs is a
different mechanism — Compose mounts it at the same /run/secrets/<id> path,
but from a secrets: block rather than a --secret flag:
services:
myservice:
image: example/image
secrets:
- account_token
environment:
- ACCOUNT_TOKEN=/run/secrets/account_token
secrets:
account_token:
file: account_token.txt
Note what the environment variable holds: the path to the secret, not the
secret. Services that expect the value inline need reading from that path
instead, and passing the value directly through environment: puts it in
docker inspect — which is the thing this avoids.
The backing file (account_token.txt here) sits next to the Compose file and
must be covered by .gitignore before it is created.
See also#
Code quality tooling — the gate list, including the secret scan
.gitleaks.toml— the scan’s allowlist: each entry pins one value and needs a written justification; needing a new one usually means a real secret got committed..gitleaksignoreis kept empty on purpose.