Suggest an editImprove this articleRefine the answer for “How to use environment variables in Docker?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Docker has four ways to set env vars:** `ENV` in Dockerfile, `-e KEY=value` on `docker run`, `--env-file file.env`, and `environment:` in Compose. ```bash # Dockerfile ENV NODE_ENV=production # docker run docker run -e DATABASE_URL=postgres://... -e LOG_LEVEL=debug myapp docker run --env-file .env myapp # Compose services: api: environment: - NODE_ENV=production env_file: .env ``` **Key:** never put secrets in env vars in production — they leak into `docker inspect`, image history, and process lists. Use BuildKit secret mounts or external secret managers instead.Shown above the full answer for quick recall.Answer (EN)Image**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.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.