News
🛠️ DevOps Tutorials Environment Variables in Docker: .env Files, Secrets, and Build Args

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:

  1. ENV in the Dockerfile (baked into image)
  2. environment: in compose.yml (explicit value)
  3. Variable substitution from .env in compose.yml
  4. -e or --env-file at docker run time

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.