Docker from Basics to Production · Production-Ready Images

Configuration and Secrets

Keep images environment-agnostic: configuration through environment variables and files, secrets out of layers and history.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

An image that takes its configuration at run time, and a demonstration of how secrets leak when passed as build arguments, compared with BuildKit secret mounts and Compose secrets.

The idea

Build once, run anywhere: the same image goes to development, staging and production, so nothing environment-specific belongs in it.

  • Configuration comes from environment variables or mounted config files at run time.
  • Secrets must not appear in image layers, the image config (ENV) or the build history (ARG):
    • Build-time secrets (private package registries): RUN --mount=type=secret,id=....
    • Runtime secrets: files mounted by the platform (Compose secrets:, Kubernetes Secrets, a cloud secret manager), read from /run/secrets/....

The code

A build argument leaks; a secret mount doesn't
trap 'docker image rm -f rc-leaky:1 rc-safe:1 > /dev/null 2>&1' EXIT
echo "token-123-secret" > npm_token.txt
cat > Dockerfile.leaky <<'EOF'
FROM alpine:3.24
ARG NPM_TOKEN
RUN echo "using token to install packages..." && test -n "$NPM_TOKEN"
EOF
cat > Dockerfile.safe <<'EOF'
FROM alpine:3.24
RUN --mount=type=secret,id=npm_token test -s /run/secrets/npm_token && echo "using mounted token to install packages..."
EOF
docker build -q -f Dockerfile.leaky --build-arg NPM_TOKEN="$(cat npm_token.txt)" -t rc-leaky:1 . > /dev/null
docker build -q -f Dockerfile.safe --secret id=npm_token,src=npm_token.txt -t rc-safe:1 . > /dev/null
echo "history of the ARG image mentions the token: $(docker history --no-trunc rc-leaky:1 | grep -c token-123-secret)"
echo "history of the secret-mount image mentions it: $(docker history --no-trunc rc-safe:1 | grep -c token-123-secret)"
mkdir -p secrets && echo "db-password-from-file" > secrets/db_password
cat > compose.yaml <<'EOF'
services:
  app:
    image: alpine:3.24
    command: sh -c 'echo "app read $(wc -c < /run/secrets/db_password) bytes from /run/secrets/db_password"'
    secrets: [db_password]
secrets:
  db_password:
    file: ./secrets/db_password
EOF
docker compose -p rc-secrets run --rm app 2>/dev/null
docker compose -p rc-secrets down > /dev/null 2>&1

Output

history of the ARG image mentions the token: 2
history of the secret-mount image mentions it: 0
app read 22 bytes from /run/secrets/db_password

Run this on your computer — see “Get Started” for setup.

Common mistakes

  • Building one image per environment with configuration baked in.
  • Passing tokens as --build-arg (visible in docker history) or ENV (visible in docker inspect).
  • Printing secrets in logs during startup.
  • Mounting a .env with all secrets into every service instead of giving each service only what it needs.

Key points

  • One image for every environment; configuration at run time.
  • Build-time secrets via BuildKit secret mounts, runtime secrets via mounted files from the platform.
  • Check docker history --no-trunc and docker inspect for leaks.

Further reading: Build secrets · Secrets in Compose

Practice questions

Why is passing a token with --build-arg unsafe?

Answer

Build arguments used in RUN steps are recorded in the image history, so anyone with the image can read them with docker history --no-trunc.

How do Compose secrets reach the container?

Answer

As files mounted under /run/secrets/<name>, which the application reads at runtime; they are not environment variables or image layers.

What does 'build once, run anywhere' mean for image configuration?

Answer

The same image is promoted through all environments, so environment-specific configuration and secrets are supplied at run time rather than baked into the image.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?