ENV & ARG
Inject variables into your builds and containers. Learn the critical difference between ENV (runtime) and ARG (build-time only), and how to use both safely.
ENV & ARG
Both ENV and ARG define variables in a Dockerfile, but they live in completely different phases. Confusing them leads to either variables that don't exist at runtime or secrets being baked into the image. The distinction is straightforward once you see it clearly.
The Core Difference
ARG | ENV | |
|---|---|---|
| Available during | Build only | Build + Runtime |
| Visible in running container | No | Yes |
Visible in docker inspect | No | Yes |
| Override with | --build-arg | -e at docker run |
| Persists in image layers | No | Yes |
| Safe for secrets | No (still visible in history) | No |
ENV - Runtime Environment Variables
ENV sets variables that are available both during the build (in subsequent RUN instructions) and inside every container created from the image.
ENV NODE_ENV=production
ENV PORT=3000
ENV LOG_LEVEL=info
# Multiple in one instruction (preferred)
ENV NODE_ENV=production \
PORT=3000 \
LOG_LEVEL=info
ENV values are baked into the image and visible in docker inspect. Override them at runtime with -e:
# Override ENV defaults at run time
docker run -e LOG_LEVEL=debug -e PORT=4000 myapp
Good uses for ENV:
- Default configuration values (
PORT,LOG_LEVEL,NODE_ENV) - Path variables (
PATH,PYTHONPATH,GOPATH) - Tool configuration (
PIP_NO_CACHE_DIR=1,NPM_CONFIG_LOGLEVEL=warn)
Never use ENV for secrets - they're stored in the image and visible via docker inspect.
ARG - Build-Time Variables
ARG variables only exist during docker build. They're gone when the container runs.
ARG APP_VERSION=1.0.0
ARG BUILD_DATE
ARG GIT_COMMIT
RUN echo "Building version $APP_VERSION on $BUILD_DATE ($GIT_COMMIT)"
Pass them at build time with --build-arg:
docker build \
--build-arg APP_VERSION=2.1.0 \
--build-arg BUILD_DATE=$(date -u +%Y-%m-%d) \
--build-arg GIT_COMMIT=$(git rev-parse --short HEAD) \
-t myapp:2.1.0 .
If no --build-arg is passed and no default is set, the variable is empty.
Using ARG Before FROM
ARG is the only instruction that can appear before FROM. Use it to parameterize the base image version:
ARG NODE_VERSION=20
FROM node:${NODE_VERSION}-alpine
# This ARG is scoped before FROM - need to re-declare to use after FROM
ARG NODE_VERSION
RUN echo "Running on Node $NODE_VERSION"
Note: An
ARGbeforeFROMis in a different scope. To use it afterFROM, declare it again (with or without a default).
This pattern lets you build the same Dockerfile with different base versions:
docker build --build-arg NODE_VERSION=18 -t myapp:node18 .
docker build --build-arg NODE_VERSION=20 -t myapp:node20 .
Combining ARG and ENV
A common pattern - use ARG to accept a build-time value, then promote it to ENV so it's available at runtime:
ARG APP_VERSION=1.0.0
ENV APP_VERSION=${APP_VERSION}
ARG PORT=3000
ENV PORT=${PORT}
# Build with a specific version, available at runtime
docker build --build-arg APP_VERSION=2.5.0 -t myapp:2.5.0 .
docker run myapp:2.5.0
# Inside container: echo $APP_VERSION → 2.5.0
Parameterize a Build
You want to build an image tagged myapp:3.0.0, passing a build argument APP_VERSION=3.0.0 (which the Dockerfile promotes from ARG to ENV). Then you want to run the container and print the value of APP_VERSION from inside it. Which sequence of commands is correct?
ARG and the Build Cache
ARG values affect layer caching. Changing an --build-arg value invalidates the cache from the point the ARG is first used:
ARG CACHE_BUST=1
RUN apt-get update && apt-get upgrade -y # Reruns when CACHE_BUST changes
# Force a fresh apt-get update by changing the cache busting value
docker build --build-arg CACHE_BUST=$(date +%s) -t myapp .
This is a controlled way to invalidate specific layers without rebuilding everything.
Secrets in Builds (Don't Use ARG or ENV)
A common mistake - passing secrets as ARG or ENV:
# Secret visible in docker history even after the layer is "deleted"
ARG NPM_TOKEN
RUN echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc && \
npm install && \
rm .npmrc
The secret is embedded in the layer even after .npmrc is deleted. Use BuildKit's --secret mount instead:
# Secret is never stored in any layer
RUN --mount=type=secret,id=npm_token \
echo "//registry.npmjs.org/:_authToken=$(cat /run/secrets/npm_token)" > .npmrc && \
npm install && \
rm .npmrc
docker build --secret id=npm_token,env=NPM_TOKEN -t myapp .
Predefined ARGs
Docker provides several pre-defined build arguments you can use without declaring them:
# Available without ARG declaration
RUN echo $HTTP_PROXY
RUN echo $HTTPS_PROXY
RUN echo $NO_PROXY
RUN echo $TARGETPLATFORM # e.g. linux/amd64
RUN echo $TARGETOS # e.g. linux
RUN echo $TARGETARCH # e.g. amd64
These are automatically passed from the build environment or docker build --build-arg.
Summary
ENVis for runtime configuration - visible in containers anddocker inspectARGis for build-time parameters - gone after the build completes- Use
ARGbeforeFROMto parameterize the base image version - Promote
ARGtoENVwhen you need a build argument to also be a runtime variable - Never pass secrets via
ARGorENV- use BuildKit's--secretmount flag instead - Changing an
--build-argvalue invalidates the cache from where it's first used