Skip to main content

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

  • ENV in Dockerfile = baked into the image. Visible in image history forever. For non-secret defaults.
  • -e KEY=value on docker run = per-container, not in image.
  • --env-file file.env = bulk env from a file. Same scope as -e.
  • Compose environment: and env_file: = runtime, project-scoped.
  • ARG in 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

dockerfile
FROM node:22-alpine ENV NODE_ENV=production ENV PORT=3000 # OR multi-line ENV NODE_ENV=production \ PORT=3000 \ LOG_LEVEL=info

Values are part of the image. Anyone running the image gets them by default. Override at run time:

bash
docker run -e PORT=8080 myapp # PORT now 8080, NODE_ENV still production

Important: ENV values appear in docker history and docker inspect. Never bake secrets here.

-e and --env-file on docker run

bash
# 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=production

No quotes, no shell expansion, one KEY=VALUE per line.

Compose: environment: and env_file:

yaml
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 unset

Compose 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

dockerfile
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 ENV
bash
docker build --build-arg BUILD_VERSION=1.2.3 -t myapp .
  • ARG = build-time only. Disappears after docker build finishes.
  • ENV = runtime. Lives in the image and the running container.
  • Common pattern: receive a value as ARG (so the build can use it), copy to ENV (so the runtime can see it).

Why secrets do NOT belong in env

bash
docker run -e DB_PASSWORD=hunter2 myapp

Leaks:

  • docker inspect <container> shows env in plain text
  • ps auxe on the host can show env of the container's PID 1
  • Container's own /proc/1/environ is 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 --mount=type=secret,id=npmrc 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.local outside git is fine

Common mistakes

Putting passwords in ENV in Dockerfile

dockerfile
# WRONG: forever in image history ENV DB_PASSWORD=hunter2

The 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 spaces

Docker .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

yaml
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 vars

Two 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

dockerfile
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 time

Dockerfile 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.yaml references 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

yaml
# 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
bash
# .env (Compose interpolation) TAG=v1.2.3 DB_PASSWORD=hunter2 # .env.app (passed to api container as env) FEATURE_X=enabled FEATURE_Y=disabled
bash
$ 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

dockerfile
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"]
bash
$ docker build --build-arg BUILD_VERSION=1.2.3 -t demo . $ docker run --rm demo Running version 1.2.3 1.2.3

ARG passes value into build; copying to ENV makes it available at runtime.

BuildKit secret for an npmrc

dockerfile
# syntax=docker/dockerfile:1.7 FROM node:22-alpine WORKDIR /app COPY package*.json ./ RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \ npm ci COPY . .
bash
$ 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 ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Comments

No comments yet