Environment Variables in Docker: .env Files, Secrets, and Build Args
Pass configuration into containers the right way — keeping credentials out of images and Dockerfiles, and keeping dev and prod configs separate.
Environment variables are how you configure a containerised application without baking values into the image. The mechanics are simple, but there are a few ways to get it wrong that cause either security problems or confusing behaviour.
The basics: ENV and -e
Set a variable in a Dockerfile:
ENV NODE_ENV=production
ENV PORT=3000
These are baked into the image. Every container started from this image inherits them. Use ENV for defaults that rarely change and that are safe to be visible in the image.
Override them at runtime:
docker run -e NODE_ENV=staging -e PORT=8080 myapp
Runtime values override image defaults. The image still has NODE_ENV=production baked in, but any container started with -e NODE_ENV=staging will see staging.
.env files
Passing ten -e flags to docker run is tedious. Put them in a file instead:
# .env
DATABASE_URL=postgres://app:secret@db:5432/mydb
REDIS_URL=redis://cache:6379
SECRET_KEY=dev-key-replace-in-production
Pass the file to docker run:
docker run --env-file .env myapp
Do not COPY the .env file into the image. --env-file reads the file on the host and injects the values as environment variables — the file itself never enters the container. Copying it would bake the credentials into every layer of the image, where they would persist even if you deleted the file in a later step.
Add .env to .gitignore. Commit a .env.example with placeholder values so teammates know what to provide.
Docker Compose and .env
Compose has native .env support. Place a .env file next to compose.yml and Compose reads it automatically. Use variable substitution in the compose file:
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ${DB_NAME}
# .env
DB_USER=app
DB_PASSWORD=secret
DB_NAME=mydb
The values are substituted at docker compose up time. The compose file itself stays clean of credentials and is safe to commit.
To use a different env file:
docker compose --env-file .env.staging up
Build arguments vs environment variables
ARG values are available during the build process but do not persist into the image or into running containers.
ARG APP_VERSION=unknown
RUN echo "Building version ${APP_VERSION}"
ENV APP_VERSION=${APP_VERSION} # copy into ENV if you need it at runtime
Pass build args:
docker build --build-arg APP_VERSION=1.4.2 .
Do not use ARG for secrets. Build args are visible in docker history and in the image metadata, even if the value is never written to a file inside the container. A secret passed as a build arg is exposed to anyone who can inspect the image.
Secrets at build time (BuildKit)
For credentials needed during the build — a private npm registry token, an SSH key for a private repo — use BuildKit's --secret flag:
# syntax=docker/dockerfile:1.7
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
npm ci
docker build --secret id=npmrc,src=.npmrc .
The secret is mounted as a file during that single RUN step and is never written to any image layer. docker history does not show it. This is the correct approach for anything sensitive that is needed at build time.
Verify what variables a container actually sees
docker exec container_name env
Or inspect a specific variable:
docker exec container_name sh -c "echo \$DATABASE_URL"
Useful for confirming that a variable from a .env file is actually reaching the container, or that a default from the Dockerfile is being overridden correctly.
The hierarchy
When the same variable is set in multiple places, the order of precedence from lowest to highest:
ENVin the Dockerfile (baked into image)environment:incompose.yml(explicit value)- Variable substitution from
.envincompose.yml -eor--env-fileatdocker runtime
The last value set wins. If a variable appears both in the Dockerfile and in --env-file, the --env-file value takes effect in the running container.
SysEmperor