DockerIntermediate 12 min Lesson 8 of 10

Environment Variables and Secrets

Configure containers without rebuilding, keep secrets out of images and Compose files, and know where environment variables are visible.

Docker · Lesson 8 of 10
0/10 done(0%)

What is it? #

Containers are configured with environment variables. The same image runs in development, staging and production with different values.

That is the core discipline: configuration comes from outside the image, so one build is promoted through environments rather than rebuilt per environment.

Secrets need more care. Build arguments are visible in image history, environment variables are visible to anyone who can inspect the container, and both are easy to commit by accident.

For anything genuinely sensitive, file-based secrets are better than environment variables, because they are not exposed in process inspection or crash dumps.

Think of it like this #

A machine with a settings dial rather than a fixed factory configuration.

The same machine works in every workshop. Where it gets awkward is the key to the safe: writing it on the dial where everyone can read it defeats the purpose.

Simple example #

An application image built once, run locally with development values, in CI with test values and in production with real credentials — no rebuild between them.

Code #

BASH
# Passing variables at run time
docker run -e LOG_LEVEL=debug -e PAGE_SIZE=20 myapp
docker run --env-file .env.production myapp     # a file of KEY=value lines
YAML
# In Compose
services:
  app:
    image: myapp:1.4.2
    env_file:
      - .env                         # not committed
    environment:
      LOG_LEVEL: info                # non-sensitive, fine inline
      DATABASE_URL: postgresql://appuser:${DB_PASSWORD}@db:5432/shop
DOCKERFILE
# ENV in the Dockerfile: defaults and non-secret settings ONLY
ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    LOG_LEVEL=info

# NEVER do this — it is baked into the image and visible in history
# ENV API_KEY=sk_live_abc123
DOCKERFILE
# Build-time secrets, done correctly
# BAD: ARG values remain visible in the image history
# ARG NPM_TOKEN
# RUN npm config set //registry.npmjs.org/:_authToken=$NPM_TOKEN

# GOOD: a build secret, mounted only for that instruction
RUN --mount=type=secret,id=npm_token \
    NPM_TOKEN=$(cat /run/secrets/npm_token) npm ci

# docker build --secret id=npm_token,src=./npm_token.txt -t myapp .
YAML
# File-based secrets in Compose — safer than environment variables
services:
  app:
    image: myapp:1.4.2
    secrets:
      - db_password
    environment:
      DB_PASSWORD_FILE: /run/secrets/db_password    # the app reads the file

secrets:
  db_password:
    file: ./secrets/db_password.txt                  # 600, not committed
TEXT
Where environment variables are visible

docker inspect <container>              shows every variable
docker exec <container> env             shows them
/proc/<pid>/environ on the host         readable by root
crash dumps and error reports           often include the environment
child processes                         inherit them

File-based secrets avoid all of these: the value is read from a file
with restricted permissions and never enters the process environment.

How it works #

Environment variables set at run time override those in the image, which is what allows one image to behave differently per environment.

ENV in a Dockerfile is baked into the image. It is appropriate for defaults and runtime behaviour flags, and never for credentials — anyone who can pull the image can read them.

Build arguments are worse than they look. ARG values appear in docker history, so a token passed that way is recoverable from the image even though it is not in the final filesystem.

The --mount=type=secret form makes the value available only during that instruction and never writes it into a layer. It is the correct mechanism for private registry tokens during a build.

File-based secrets mount the value as a file inside the container. The application reads the file, so the secret never appears in docker inspect or in the process environment — which matters because crash reporters frequently capture the environment.

The convention of DB_PASSWORD_FILE pointing at a path, rather than DB_PASSWORD holding the value, is common in official images and is worth supporting in your own.

Variable substitution in Compose, ${DB_PASSWORD}, reads from the shell or a .env file at the project root. That file must be git-ignored.

Real-world use #

The build-once, configure-per-environment discipline is what makes promotion pipelines work: the exact artefact tested in staging is the one deployed to production.

Committed secrets remain among the most common real incidents. Scanners monitor public repositories constantly and credentials are abused within minutes.

Orchestrators provide proper secret management — mounted as files, encrypted at rest, with access control and rotation. Compose secrets are a simpler version of the same idea.

Cloud secret managers go further with automatic rotation and audit logging, injecting values at container start.

Whichever mechanism you use, the rules are constant: never in the image, never in git, and rotate when exposed.

Common mistakes #

  • Putting secrets in ENV or ARG, where they persist in image history.
  • Committing a .env file with real credentials.
  • Assuming environment variables are private — they are visible to inspection.
  • Rebuilding the image per environment instead of configuring at run time.
  • Logging the full environment during startup or in error reports.

Practice #

Build an image with sensible ENV defaults and no secrets. Run it with an env file for configuration, then convert the most sensitive value to a file-based secret and confirm it no longer appears in the container environment.

Quick quiz

  1. 1. Why should secrets never be set with ENV in a Dockerfile?

  2. 2. What is the problem with passing a token as a build ARG?

  3. 3. Why are file-based secrets safer than environment variables?

  4. 4. What does build-once, configure-per-environment enable?

  5. 5. Where does Compose read `${DB_PASSWORD}` from?

Summary

  • Configure containers at run time so one image serves every environment.
  • Never put secrets in ENV or ARG — both persist in the image.
  • Use build secrets for private tokens during a build.
  • Prefer file-based secrets over environment variables for sensitive values.
  • Keep env files out of git and rotate anything exposed.