There are four ways to put values into a container: ENV in the Dockerfile, -e / --env-file at run time, Compose's environment: / env_file:, and (build-time only) ARG. They cover different lifecycle stages and have different security characteristics. The one to be careful about is anything that touches secrets at build time — those leak into the image's history.
ENV (runtime) vs ARG (build time)
ARG NODE_VERSION=22
FROM node:${NODE_VERSION}-alpine
ENV NODE_ENV=productionARG is available only at build time. Used by FROM, RUN, COPY, etc. Disappears when the image is built; the running container can't see it.
ENV sets an environment variable that exists at both build time (for later RUN instructions) and persists into the running container. The container's processes see it via the standard process.env / os.environ / $ENV lookup.
Rule of thumb:
- Need a value at build time only (like a base image version, a Git ref to clone)?
ARG. - Need it in the running container?
ENV. - Need it at both? Pair them:
ARG X+ENV X=$X.
Passing values at build time
docker build --build-arg NODE_VERSION=22 -t my-app .That sets the ARG NODE_VERSION declared in the Dockerfile.
You can also have an ARG with no default and require the build to pass one:
ARG GIT_COMMIT
RUN echo "Built from commit $GIT_COMMIT" > /etc/built-fromdocker build --build-arg GIT_COMMIT=$(git rev-parse HEAD) -t my-app .Why --build-arg is bad for secrets
docker build --build-arg API_KEY=supersecret -t my-app . # DON'TThat value ends up in the image's build history. Anyone with image-pull access can run docker history my-app --no-trunc and see the value. Same for the inspect output.
Use BuildKit's secret mount instead:
RUN --mount=type=secret,id=api-key cat /run/secrets/api-key && do-somethingdocker build --secret id=api-key,src=./api-key.txt -t my-app .The secret is mounted at /run/secrets/api-key during the RUN only, doesn't appear in any layer, doesn't appear in image history. BuildKit is the default builder in Docker 23.0+ so this works out of the box.
Passing env vars at run time
# Single var
docker run -e KEY=VALUE :image
# Multiple
docker run -e KEY1=value1 -e KEY2=value2 :image
# Pass through the host's env var of the same name (no = sign)
docker run -e DATABASE_URL :image
# Load from a file
docker run --env-file .env :image--env-file reads a file of KEY=VALUE lines. Comments (#), blank lines, and inline whitespace are tolerated; quotes are NOT processed — KEY="value" ends up as KEY set to the literal "value" (with the quotes). Strip quotes from your .env before passing.
A typical .env:
DATABASE_URL=postgres://user:pass@db:5432/myapp
REDIS_URL=redis://cache:6379
API_KEY=...
Compose: environment and env_file
services:
app:
image: my-app
environment:
NODE_ENV: production
DATABASE_URL: postgres://user:pass@db:5432/myapp
env_file:
- .env
- .env.productionTwo distinct mechanisms:
environment:— explicit key-value pairs in the YAML. Visible indocker compose config.env_file:— load from one or more.envfiles. Each file is plainKEY=VALUElines, same format asdocker run --env-file.
Both end up as environment variables inside the container.
Compose variable substitution: a separate thing
Compose also has variable substitution inside the YAML itself:
services:
db:
image: postgres:17
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD} # substituted from the host's env${DB_PASSWORD} is substituted at docker compose up time, using:
- The host's shell environment, or
- A
.envfile in the same directory asdocker-compose.yml.
That .env file is a Compose-specific thing — its values feed the YAML, not the container. It's a common confusion: people put DATABASE_URL=... in .env and expect it inside the container, but unless they also reference ${DATABASE_URL} somewhere in docker-compose.yml (or use env_file: .env), it's invisible to the container.
So .env next to docker-compose.yml serves two roles depending on how you reference it:
- Referenced via
${VAR}in the YAML — feeds the compose file at parse time. - Referenced via
env_file: .env— gets loaded into the container as env vars.
You can do both. They're not the same.
Precedence: which value wins
When the same env var is set in multiple places, this is the order (later wins):
ENVin the Dockerfile (lowest precedence)- Compose
env_file:entries - Compose
environment:block - Compose YAML variable substitution
- Shell environment passed to
docker compose up -eondocker run(highest precedence)
So -e KEY=runtime on docker run always wins over ENV KEY=image in the Dockerfile. Useful for overriding image defaults at deployment.
A clean pattern for secrets
The setup that scales:
- Non-sensitive config (
NODE_ENV,LOG_LEVEL, public URLs) → DockerfileENVor Composeenvironment:. - Secrets (database passwords, API keys, JWT secrets) → Compose
env_file: .envordocker run --env-file, where.envis gitignored and never committed. - CI / production secrets → injected by your orchestrator (Kubernetes secrets, Compose with secrets in the host env, Docker Swarm secrets, AWS Parameter Store, etc.).
- Build-time secrets → BuildKit
--secretmount, never--build-arg.
The line that catches most teams once: .env in .dockerignore so COPY . . doesn't ship it into the image. See .dockerignore Best Practices.
Common pitfalls
- Putting secrets in
--build-arg. They end up in image history. Use BuildKit's--mount=type=secretinstead. - Expecting
.envnext todocker-compose.ymlto land in the container. It feeds YAML substitution, not the container, unless you also reference it inenv_file:. - Copying
.envinto the image. Even if it's a runtime config, baking it into the image kills the ability to deploy the same image to multiple environments. Pass at runtime. environment: ARRAY_FORMATsyntax confusion. Compose accepts both map and list forms:Map form is more readable; list form is required if your key contains characters YAML doesn't like as keys (rare).yamlenvironment: KEY: value # map form environment: - KEY=value # list form- Quotes in
.envfiles getting included literally.KEY="value"in.envis parsed asKEYset to"value"(with the quotes). Strip the quotes.
What to do next
- How to Write a Dockerfile — Dockerfile reference including
ARGandENV. - .dockerignore Best Practices — the file that should always ignore
.env. - Docker Compose: Getting Started — Compose context including the variable-substitution mechanism.
FAQ
Sources
Authoritative references this article was fact-checked against.
- docker run --env referencedocs.docker.com
- ARG and ENV — Dockerfile referencedocs.docker.com

