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.
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/....
- Build-time secrets (private package registries):
The code
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 indocker history) orENV(visible indocker inspect). - Printing secrets in logs during startup.
- Mounting a
.envwith 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-truncanddocker inspectfor 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.