How do ARG and ENV differ in a Dockerfile?
Quick Answer
ARG defines a variable that only exists during the image build — it's not available in the running container at all, unless you also explicitly set it as an ENV. ENV defines a variable that's baked into the image and persists into every container started from it, available to the application at runtime. Use ARG for build-time configuration, like a version number to install or a build-target flag, and ENV for anything the running application itself needs to read.
Detailed Answer
ARG — build-time only, not present in the running container
ARG NODE_VERSION=20
FROM node:${NODE_VERSION}-slim
ARG BUILD_ENV=production
RUN if [ "$BUILD_ENV" = "development" ]; then npm install; else npm ci --omit=dev; fi
docker build --build-arg NODE_VERSION=18 --build-arg BUILD_ENV=development -t myapp .
ARG values are supplied at build time, via --build-arg or a default in the Dockerfile, and are only accessible during the build. Once the image is built and a container starts from it, none of these ARG values exist in the running container's environment — unless you deliberately also expose them via ENV.
ENV — persists into the running container
ENV NODE_ENV=production
ENV PORT=3000
docker run myapp env | grep NODE_ENV
# NODE_ENV=production
ENV values are baked into the image and automatically present as environment variables in every container started from it. This is what the running application actually reads through normal environment-variable access, like process.env.NODE_ENV in Node or os.environ in Python.
Bridging the two: using an ARG to set a default ENV value
ARG APP_VERSION=dev
ENV APP_VERSION=${APP_VERSION}
docker build --build-arg APP_VERSION=2.1.0 -t myapp .
docker run myapp env | grep APP_VERSION
# APP_VERSION=2.1.0
This lets a build-time value — say, one injected by CI reflecting the actual git tag or commit being built — become available to the running application too. Without this bridging, an ARG's value would be invisible once the container is actually running. This is the detail most likely to cause confusion.
Why ARG values should never hold secrets
# NEVER do this
ARG API_KEY
RUN curl -H "Authorization: Bearer $API_KEY" https://example.com/fetch-something
Even though ARG values aren't automatically present in the running container's environment, they are recorded in the image's build history and visible to anyone who can inspect the image. docker history can reveal build-time ARG/command details, depending on how they were used. Passing secrets via ARG is a common, real security mistake — the value can end up baked into a layer's metadata even though it's not "in the environment" the way ENV values are. Genuine build-time secrets, like a private package registry token needed only during npm install, should use Docker's dedicated build secrets mechanism instead: RUN --mount=type=secret, part of BuildKit. It makes a secret available only during one RUN instruction's execution, without persisting it in any layer or image metadata.