1161 words
6 minutes

Docker Build Secrets: Use --secret Instead of ARG or ENV

2026-08-16
DevOps
Docker
/
Containers
/
DevOps
/
Troubleshooting

Use a BuildKit secret mount when a Docker build needs a token, private package credential, or SSH key. Pass it with docker build --secret, consume it with RUN --mount=type=secret, and never put the value in ARG, ENV, or a copied file.

The secret mount is temporary: the value is available to the single RUN instruction and is not copied into the resulting image by the mount itself. That makes it the right boundary for private package installs and authenticated Git operations. It does not protect a command that prints the secret or writes it into an output file, so the command still has to treat the mounted value as sensitive.

Why ARG and ENV are the wrong place for build credentials#

This pattern looks convenient but leaks the credential into build metadata or the image configuration:

ARG NPM_TOKEN
RUN npm config set //registry.npmjs.org/:_authToken=$NPM_TOKEN && npm ci

Build arguments are not designed for secrets. Docker documents that values used in ARG can be visible in the image history, and modern BuildKit provenance can also record build arguments. An ENV value is even more obviously part of the image configuration and is inherited by later layers and containers.

The Dockerfile reference recommends secret mounts instead. A useful rule is:

Build inputUse it forKeep it out of
ARGA non-sensitive version or feature switchPasswords, tokens, private keys
ENVA runtime setting that the image may intentionally exposeCredentials that should not reach the container
RUN --mount=type=secretA credential needed by one build commandThe final image and build layers
RUN --mount=type=sshAn SSH agent used by a private Git operationCopied key files and image layers

If a value determines the output of a build, remember that a secret’s contents are not included in BuildKit’s cache checksum. A cached RUN can therefore be reused even after the secret changes; the cache invalidation guide explains how to add a non-secret cache-busting input when that behavior matters.

Mount a file secret with docker build --secret#

For a private npm registry, keep the local .npmrc outside the build context when possible and pass it as a secret source:

# 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
COPY . .
CMD ["npm", "start"]

Build the image with the matching secret ID:

Terminal window
docker build \
--secret id=npmrc,src="$HOME/.npmrc" \
--tag example-app:build-secrets .

The id connects the command-line source to the Dockerfile mount. The default mount location is /run/secrets/<id>, but target makes the path explicit for tools such as npm that already look for a configuration file. required=true changes a missing credential from a confusing package-install failure into an immediate mount error.

Do not add the secret file to COPY, and do not use a build argument to interpolate its contents into a RUN command. The secret is mounted only while that instruction runs. If the package manager creates a cache containing credentials, configure that cache separately and inspect what the tool writes.

Use an environment secret when the tool expects a variable#

BuildKit can read the source from the client environment instead of a file:

Terminal window
export GIT_AUTH_TOKEN='use-a-short-lived-token'
docker build \
--secret id=GIT_AUTH_TOKEN,env=GIT_AUTH_TOKEN \
--tag example-app:private-dependency .

Consume it in the one command that needs it:

RUN --mount=type=secret,id=GIT_AUTH_TOKEN,env=GIT_AUTH_TOKEN,required=true \
./scripts/download-private-dependency.sh

The command should read the variable and avoid echoing its environment. Prefer a short-lived, least-privilege token, and make sure CI masks the source variable in logs. The env option changes how the secret is exposed inside the instruction; it does not turn the value into a normal image-wide ENV variable.

Use an SSH mount for private Git dependencies#

An SSH agent is a different credential shape. Do not copy a private key into the image. Forward the agent only to the clone or install command:

# syntax=docker/dockerfile:1
FROM alpine:3.20
RUN apk add --no-cache git openssh-client
RUN mkdir -p -m 0700 /root/.ssh \
&& ssh-keyscan github.com >> /root/.ssh/known_hosts
RUN --mount=type=ssh \
git clone [email protected]:example/private-library.git /opt/private-library

Build with the SSH agent that is already loaded on the client:

Terminal window
docker build --ssh default --tag example-app:private-git .

Use required=true on the SSH mount when the build must fail rather than silently falling back to an unauthenticated URL:

RUN --mount=type=ssh,id=default,required=true \
git clone [email protected]:example/private-library.git /opt/private-library

The SSH mount forwards agent access for the instruction; it does not make the private key available as a normal file in the image.

Debug a secret that is “not available”#

When the build cannot see the credential, check the ID and source independently:

  1. Confirm the --secret id=... ID exactly matches the Dockerfile id=....
  2. Confirm the source file exists or the environment source is set in the process that invokes docker build.
  3. Add required=true while diagnosing so a missing mount fails at the boundary.
  4. Check that the command reads the mounted path or environment name you selected.
  5. Confirm the build is using BuildKit and the Dockerfile syntax version supports the mount.
  6. Re-run with a harmless existence check, never with a command that prints the secret.

For a file mount, this test verifies only that the file is present:

RUN --mount=type=secret,id=license,target=/run/secrets/license,required=true \
test -s /run/secrets/license

Do not replace the check with cat /run/secrets/license. Build logs are another output channel, and a secret that is absent from the final image can still be exposed by the build log.

Verify the image and build configuration#

After a successful build, inspect the history and the generated image without printing the credential:

Terminal window
docker history --no-trunc example-app:build-secrets
docker image inspect example-app:build-secrets

Look for accidental ARG, ENV, or shell commands containing the credential. These checks cannot prove that an arbitrary build script did not persist a secret, so also review the command that consumed the mount and any files it generated. If a real credential was exposed in history, logs, a layer, or a deployed image, rotate it and rebuild; deleting the Dockerfile line does not revoke a leaked token.

If the failure is instead COPY failed: file not found, debug the build context and .dockerignore boundary with Docker COPY failed: file not found. Secret mounts solve credential handling, not files that were never sent to the builder.

The practical rule is simple: use ARG for public build choices, ENV for intentional runtime configuration, and secret or SSH mounts for credentials that exist only while one build instruction runs.

FAQ#

Are Docker build secrets included in the final image?#

The secret mount itself is temporary and is not copied into the image layer. A command can still leak it by writing the value into a file, output, generated artifact, cache, or log, so review the command that consumes it.

Why does Docker history show my build token?#

The token was probably passed through ARG, ENV, or a shell command whose text became build metadata. Rotate the token, remove the insecure input, and rebuild with a BuildKit secret mount.

Does --secret work without BuildKit?#

Secret mounts are a BuildKit Dockerfile feature. Use a current Docker build flow with BuildKit support and the RUN --mount=type=secret syntax; do not silently fall back to ARG for older tooling.

Should I use a secret mount or an SSH mount for a private Git repository?#

Use a secret mount for a token or credential file that the Git client expects, and an SSH mount when the repository is accessed through an SSH agent. In both cases, forward only the credential needed by the specific command.

References:

Docker Docs: Build secrets

Dockerfile reference

Docker Docs: Invalidation of build caches

Docker Build Secrets: Use --secret Instead of ARG or ENV
https://laplusda.com/en/posts/docker-build-secrets-vs-arg/
Author
Zero
Published at
2026-08-16
License
CC BY-NC-SA 4.0
Was this article useful?

Report a typo or broken link, or suggest a related topic.