How to use environment variables in Docker?
Environment variables are how Docker passes runtime configuration to the app inside a container. There are four places to set them, each with different scope and persistence. Knowing the differences keeps secrets out of your image history.
Theory
TL;DR
ENVin Dockerfile = baked into the image. Visible in image history forever. For non-secret defaults.-e KEY=valueondocker run= per-container, not in image.--env-file file.env= bulk env from a file. Same scope as-e.- Compose
environment:andenv_file:= runtime, project-scoped. ARGin Dockerfile ≠ env var. Build-time only, gone at runtime.- Never use env for secrets in prod. They appear in
docker inspect,ps aux, image history, and many logs.
ENV in Dockerfile
FROM node:22-alpine
ENV NODE_ENV=production
ENV PORT=3000
# OR multi-line
ENV NODE_ENV=production \
PORT=3000 \
LOG_LEVEL=infoValues are part of the image. Anyone running the image gets them by default. Override at run time:
docker run -e PORT=8080 myapp # PORT now 8080, NODE_ENV still productionImportant: ENV values appear in docker history and docker inspect. Never bake secrets here.
-e and --env-file on docker run
# Single var
docker run -e DATABASE_URL=postgres://... myapp
# Multiple
docker run -e DB_HOST=db -e DB_PORT=5432 -e DB_USER=postgres myapp
# Pass through from your shell (no value = read host env)
export API_KEY=secret123
docker run -e API_KEY myapp
# Bulk from a file
docker run --env-file .env myapp.env file format:
# Lines beginning with # are comments
DB_HOST=db
DB_PORT=5432
NODE_ENV=productionNo quotes, no shell expansion, one KEY=VALUE per line.
Compose: environment: and env_file:
services:
api:
image: myapp
environment:
NODE_ENV: production # map form
DATABASE_URL: postgres://...
DEBUG: "" # empty value (different from absent)
env_file:
- .env # file form
- .env.local # later overrides earlier
db:
image: postgres:16
environment:
- POSTGRES_PASSWORD=${DB_PASSWORD} # list form, with shell-style interpolation
- POSTGRES_DB=${DB_NAME:-app} # default if DB_NAME unsetCompose interpolation: ${VAR} and ${VAR:-default} reference variables from your shell or from a file named .env next to compose.yaml. The .env here is for Compose itself, used during YAML parsing. Different from env_file: which is passed to the container.
ARG vs ENV
ARG NODE_VERSION=22
FROM node:${NODE_VERSION}-alpine
ARG BUILD_VERSION # passed via --build-arg, available DURING build only
RUN echo "Building $BUILD_VERSION" > /app/version.txt
ENV APP_VERSION=$BUILD_VERSION # to make it available at runtime, copy ARG to ENVdocker build --build-arg BUILD_VERSION=1.2.3 -t myapp .ARG= build-time only. Disappears afterdocker buildfinishes.ENV= runtime. Lives in the image and the running container.- Common pattern: receive a value as
ARG(so the build can use it), copy toENV(so the runtime can see it).
Why secrets do NOT belong in env
docker run -e DB_PASSWORD=hunter2 myappLeaks:
docker inspect <container>shows env in plain textps auxeon the host can show env of the container's PID 1- Container's own
/proc/1/environis readable from inside - Many app frameworks log full env at startup (especially in dev)
- Children processes inherit env
Do this instead:
- BuildKit secret mounts for build-time secrets:
dockerfile
RUN cp /run/secrets/npmrc ~/.npmrc && npm ci - Docker Swarm secrets or K8s secrets mounted as files at runtime
- External secret managers (AWS Secrets Manager, HashiCorp Vault) read at startup
- For local dev:
.env.localoutside git is fine
Common mistakes
Putting passwords in ENV in Dockerfile
# WRONG: forever in image history
ENV DB_PASSWORD=hunter2The password is in the image manifest, visible to anyone who pulls the image. Never bake secrets.
Quoting in .env files
# WRONG: literal quotes become part of the value
VAR="value with spaces" # value is literally `"value with spaces"`
# RIGHT (Docker treats values as plain strings)
VAR=value with spacesDocker .env is not shell. No quoting needed. If you put quotes, they become part of the value.
Forgetting .env is for Compose interpolation, env_file: is for the container
services:
api:
environment:
DB_PASSWORD: ${DB_PASSWORD} # ← read from host shell or .env at YAML parse
env_file: .env.app # ← passed to container as env varsTwo different files, two different scopes. .env (default Compose lookup) is for the YAML; env_file: is for the container.
Trying to interpolate at runtime in ENV
ENV PATH=$PATH:/app/bin
# Works: $PATH here is the previous ENV value
ENV DEBUG=$LOG_LEVEL
# May NOT work: depends on ARG/ENV scoping at build timeDockerfile interpolation at ENV time uses build-time context, not runtime. For runtime composition, use a shell wrapper or entrypoint script.
Real-world usage
- Twelve-factor apps: all config via env, all behavior changes through env, no config files in the image. Compose / K8s passes env at deploy time.
- CI/CD: secrets injected as env via the CI's secret store (
GITHUB_TOKEN,DOCKERHUB_TOKEN). - Local dev:
.env.local(gitignored) holds dev overrides;compose.yamlreferences via${VAR}. - Production: secrets via mounted files (Swarm secrets, K8s secrets, Vault sidecars), not raw env. Non-secret config still via env.
Follow-up questions
Q: What is the precedence when the same var is set in multiple places?
A: For Compose: env_file < environment in service < shell env. Later wins. For docker run: -e flag wins over Dockerfile ENV.
Q: How do I see a container's effective env?
A: docker exec <name> env. Shows the runtime env including ENV from image and -e at run time.
Q: Can I unset an inherited env var?
A: Set it to empty: -e DEBUG=. Note: empty is not the same as unset; process.env.DEBUG will be "" not undefined. To truly unset, build a new image with ENV DEBUG=... actually Dockerfile ENV can be removed only by absence in Dockerfile, not unset by command.
Q: What is the difference between --env-file and Compose's env_file:?
A: Same idea. --env-file is for docker run. Compose's env_file: is the same mechanism inside the YAML.
Q: (Senior) How do you safely inject secrets into a Compose-based prod deployment?
A: Use Docker Swarm secrets (file-mounted, never in env), or external secret managers that the app reads at startup, or sealed-secrets approaches with init containers that fetch and write to a tmpfs the app reads. Avoid plain env_file: with secrets in plaintext. The line not to cross: secrets in env or in image layers.
Examples
Realistic Compose with env layered correctly
# compose.yaml
services:
api:
image: myapp:${TAG:-latest}
environment:
NODE_ENV: production
LOG_LEVEL: info
DATABASE_URL: postgres://api:${DB_PASSWORD}@db:5432/app
env_file:
- .env.app # additional non-secret config
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: app# .env (Compose interpolation)
TAG=v1.2.3
DB_PASSWORD=hunter2
# .env.app (passed to api container as env)
FEATURE_X=enabled
FEATURE_Y=disabled$ docker compose up -d
# api gets:
# NODE_ENV=production (from `environment:`)
# LOG_LEVEL=info (from `environment:`)
# DATABASE_URL=postgres://api:hunter2@db:5432/app (interpolated)
# FEATURE_X=enabled, FEATURE_Y=disabled (from .env.app via env_file:)Build-time arg + runtime env
ARG BUILD_VERSION
FROM alpine:3.21
ARG BUILD_VERSION
ENV APP_VERSION=$BUILD_VERSION
RUN echo $APP_VERSION > /version.txt
CMD ["sh", "-c", "echo Running version $APP_VERSION; cat /version.txt; sleep 100"]$ docker build --build-arg BUILD_VERSION=1.2.3 -t demo .
$ docker run --rm demo
Running version 1.2.3
1.2.3ARG passes value into build; copying to ENV makes it available at runtime.
BuildKit secret for an npmrc
# syntax=docker/dockerfile:1.7
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN \
npm ci
COPY . .$ docker buildx build --secret id=npmrc,src=$HOME/.npmrc -t myapp .The .npmrc is available to the RUN step but never lands in any image layer. No ENV NPM_TOKEN=... anywhere.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.
Comments
No comments yet