ARG NPM_TOKEN looks temporary because build arguments don't reach the running container, but a build argument used by a RUN step is recorded in the image history, so docker history can reveal it. Pass build-time secrets with a BuildKit secret mount (RUN --mount=type=secret), which exposes the secret to one step without writing it to a layer or the history.
openrouting — dockerfile explainer
$ explain Dockerfile! ARG NPM_TOKEN carries a secret at build time mediumline 4fix: see below$
What the free Dockerfile explainer reports on the risky example below.
Why it matters
Build arguments are meant for non-secret build settings such as versions. When a RUN step uses one, the value is part of that step's recorded command, and docker history on the finished image shows it.
The explainer raises this at medium for a secret-named ARG without a default, and at high when the Dockerfile writes a literal default, because then the value is also readable by anyone with access to the repository.
Deleting the file a secret was written to (for example an .npmrc) in the same RUN keeps it out of the layer, but does nothing about the build argument itself.
Risky and safer Dockerfile
RiskyDockerfile
FROM node:22.11-slim
WORKDIR /app
COPY package.json package-lock.json ./
ARG NPM_TOKEN
RUN echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc \
&& npm ci --omit=dev \
&& rm .npmrc
COPY src ./src
USER 10001
HEALTHCHECK CMD ["node", "src/healthcheck.js"]
CMD ["node", "src/server.js"]
SaferDockerfile
FROM node:22.11-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=secret,id=npmrc,target=/app/.npmrc npm ci --omit=dev
COPY src ./src
USER 10001
HEALTHCHECK CMD ["node", "src/healthcheck.js"]
CMD ["node", "src/server.js"]
How to fix it
Remove the secret ARG (and any default value written in the Dockerfile).
Mount the secret only for the step that needs it: RUN --mount=type=secret,id=npmrc,target=/app/.npmrc npm ci.
Pass it at build time with docker build --secret id=npmrc,src=$HOME/.npmrc . (or the equivalent secrets setting in your CI's build action).
If the secret was ever passed as a build argument to an image that was pushed, rotate it.
When it's acceptable
An ARG whose name matches a secret pattern but holds nothing secret, such as a public key ID or a key file path.
Local builds of images that are never pushed, with a short-lived token you revoke afterwards. A secret mount is still no harder to write.
Check your own Dockerfile
The risky example is loaded below. Paste your own file to check it for this and every other Dockerfile rule. It runs in your browser; nothing is uploaded.
Runs echo "//registry.npmjs.org/:_authToken=••••••" > .npmrc && npm ci --omit=dev && rm .npmrc while building the image and saves the result as a new layer.
Sets the default command to node src/server.js. It runs when the container starts, and anything passed to docker run replaces it.
Questions
Are Docker build args visible in the final image?
Build arguments are not set in the running container's environment, but a value used by a RUN step is recorded in the image history and can be read with docker history.
How do I use a secret during docker build?
Use a BuildKit secret mount: RUN --mount=type=secret,id=mysecret,… in the Dockerfile and docker build --secret id=mysecret,src=… on the command line. The secret is available to that one step and is not written to a layer.
Why is ARG with a default value rated higher?
A literal default means the secret is written in the Dockerfile, so anyone with repository access can read it as well as anyone with the image. The explainer rates that high and a secret ARG without a default medium.