Docker · Lesson 7 of 11
Environment Variables, Config and Secrets
Configure Docker containers with environment variables, .env files and build args,, and keep secrets safe with BuildKit secret mounts and Compose secrets.
- Intermediate
- 15 min read
- 4 objectives
Before this lessonLesson 6: Multi-stage Builds and Small Images
What you will learn
- Pass configuration with -e, --env-file and Compose
- Tell ARG apart from ENV
- Keep secrets out of image layers with BuildKit secret mounts
- Use Compose secrets mounted as files
Your Progress
0 of 11 lessons 0%
- Lessons0 / 11
- Completed0
- Est. time left~ 3 hours
Create a free account to keep your progress on every device.
Tip: pressing Next marks this lesson complete automatically.
One of the promises of containers is build once, run anywhere: the exact same image runs on your laptop, in staging and in production. That only works if the image does not bake in environment-specific settings like database URLs, API keys or log levels. Those belong outside the image and get supplied when the container starts.
This follows the twelve-factor rule: store config in the environment. It also brings a hard rule for secrets: a password that ends up inside an image layer is effectively public to anyone who can pull that image.
Environment variables at run time
docker run --rm \
-e APP_ENV=staging \
-e LOG_LEVEL=debug \
alpine sh -c 'echo "env=$APP_ENV level=$LOG_LEVEL"'env=staging level=debug
If you write -e LOG_LEVEL with no value, Docker copies the variable from your current shell. For more than a couple of settings, use an env file (one KEY=value per line, no export, no quotes needed):
# staging.env
APP_ENV=staging
LOG_LEVEL=info
DATABASE_URL=postgresql://app:app@db:5432/stackconedocker run --rm --env-file staging.env alpine env | grep -E 'APP_ENV|LOG_LEVEL'APP_ENV=staging LOG_LEVEL=info
Defaults with ENV, build-time values with ARG
Two Dockerfile instructions look similar but behave very differently:
ENVsets a variable that exists at build time and in every container. Use it for sensible defaults that-ecan override.ARGexists only during the build. Pass it with--build-arg. Use it for things like a version number or a base image tag.
ARG PYTHON_VERSION=3.12
FROM python:${PYTHON_VERSION}-slim
ARG APP_VERSION=dev
ENV APP_VERSION=${APP_VERSION} \
LOG_LEVEL=info \
PORT=8000docker build --build-arg APP_VERSION=1.4.2 -t api:1.4.2 .
docker run --rm api:1.4.2 sh -c 'echo $APP_VERSION $LOG_LEVEL'
docker run --rm -e LOG_LEVEL=debug api:1.4.2 sh -c 'echo $APP_VERSION $LOG_LEVEL'1.4.2 info 1.4.2 debug
Proving the leak
It is worth seeing this once so you never forget it. Suppose someone builds with a token as a build arg:
docker build --build-arg NPM_TOKEN=npm_4bF9xQ2... -t leaky .
docker history --no-trunc leaky | grep NPM_TOKEN<missing> 3 minutes ago RUN |1 NPM_TOKEN=npm_4bF9xQ2... /bin/sh -c npm ci 184MB buildkit.dockerfile.v0
The token is sitting in plain text in the image. If this image is pushed to a public registry, the token must be revoked immediately.
Build secrets with BuildKit
Sometimes the build genuinely needs a secret, for example to install packages from a private registry. BuildKit's secret mount exposes the secret as a file only for the duration of one RUN, and it is never written to a layer:
# syntax=docker/dockerfile:1
FROM node:22-slim
WORKDIR /app
COPY package*.json .npmrc ./
RUN --mount=type=secret,id=npm_token,env=NPM_TOKEN \
npm ci --omit=dev
COPY . .# the secret comes from an environment variable on the build machine
NPM_TOKEN=npm_4bF9xQ2... docker build --secret id=npm_token,env=NPM_TOKEN -t api .Here .npmrc contains //registry.npmjs.org/:_authToken=${NPM_TOKEN}, and the env= option makes the secret available as an environment variable inside that one RUN. You can also pass a file with --secret id=npm_token,src=$HOME/.npm-token, in which case it appears at /run/secrets/npm_token. Running docker history now shows nothing sensitive.
Runtime secrets as files
Environment variables are convenient, but they are visible in docker inspect, can leak into crash reports and are inherited by every child process. For real secrets, many teams prefer mounting them as files. Compose supports this directly:
services:
api:
image: ghcr.io/stackcone/api:1.4.2
environment:
DATABASE_HOST: db
DATABASE_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
db:
image: postgres:16
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txtdocker compose up -d
docker compose exec api ls -l /run/secretstotal 4 -r--r--r-- 1 root root 25 Sep 20 11:02 db_password
The official Postgres, MySQL and many other images understand the _FILE suffix convention and read the value from the file. In your own app, read the file at startup (and fall back to the plain variable for local development). Add secrets/ to both .gitignore and .dockerignore.
Compose variable interpolation
Compose also reads a .env file next to compose.yaml and substitutes ${VAR} in the YAML itself. This is different from env_file:, which passes variables into the container:
services:
api:
image: ghcr.io/stackcone/api:${API_TAG:-latest}
env_file: .env.api # goes into the container
ports:
- "${API_PORT:-8000}:8000" # resolved by ComposeRun docker compose config to print the fully resolved file and check what Compose will actually use. In production, secrets usually come from a manager such as AWS Secrets Manager, Vault or Kubernetes Secrets rather than local files, but the application code that reads a file or variable stays the same.
Recap
- Keep environment-specific config out of the image and pass it at run time with
-e,--env-fileor Compose. ENVsets overridable defaults for containers;ARGexists only during the build.- Build args, ENV values and copied-then-deleted files are all recoverable from an image.
- Use
RUN --mount=type=secretfor secrets needed during a build. - Prefer secrets mounted as files (Compose
secrets:, the_FILEconvention) over plain environment variables at run time.
# Write your solution here
Finished reading? Mark this lesson complete to track your progress.
