Docker secrets management — keep credentials out of images
Secrets embedded in Docker images are a security disaster waiting to happen. An image pushed to a registry carries its build history — every layer, including layers where you ran RUN pip install --index-url https://user:token@registry.example.com. Even if you delete the secret in a later layer, it is still in the image. Here is how to handle secrets correctly at every phase: build time, compose, and production runtime.
The wrong way (and why it persists in the build cache)
# BAD: secret in build arg appears in docker history
FROM python:3.12-slim
ARG PYPI_TOKEN
RUN pip install --extra-index-url https://token:${PYPI_TOKEN}@private.pypi.example.com/simple/ mypackage
# Anyone who runs `docker history myimage` sees the token in the RUN command
BuildKit secret mounts (the right way for build time)
BuildKit's --mount=type=secret makes a secret available inside a RUN command without storing it in any layer:
# syntax=docker/dockerfile:1
FROM python:3.12-slim
# Secret is mounted at /run/secrets/pypi_token during this RUN only
# It does not appear in the image layer
RUN --mount=type=secret,id=pypi_token PIP_INDEX_URL="https://token:$(cat /run/secrets/pypi_token)@private.pypi.example.com/simple/" pip install mypackage
# Build with the secret passed from a file or env var
docker build --secret id=pypi_token,src=$HOME/.config/pypi-token -t myimage .
# Or from an environment variable
echo "$PYPI_TOKEN" | docker build --secret id=pypi_token,src=/dev/stdin -t myimage .
The secret file is mounted as a tmpfs inside the build container. It never touches any image layer. docker history shows nothing sensitive.
SSH keys for private git repos during build
# syntax=docker/dockerfile:1
FROM node:20-slim
# Mount SSH agent for private repo access during npm install
RUN --mount=type=ssh npm ci
# Ensure ssh-agent is running and key is added
eval $(ssh-agent)
ssh-add ~/.ssh/id_ed25519
# Build with SSH forwarding
docker build --ssh default -t myimage .
Runtime secrets with Docker Compose
For development, pass secrets as environment variables from a .env file that is gitignored:
# docker-compose.yml
services:
api:
image: myapp-api
env_file:
- .env.local # gitignored, contains DB_PASSWORD, API_KEY, etc.
environment:
# Non-secret config can be hardcoded
- NODE_ENV=development
- PORT=3000
# .env.local (gitignored)
DB_PASSWORD=dev_password_here
API_KEY=test_key_here
JWT_SECRET=local_secret_here
Docker Swarm secrets for production
In Docker Swarm, secrets are encrypted at rest and in transit, mounted as files in the container:
# Create a secret from a file
echo "supersecretpassword" | docker secret create db_password -
# Or from a file
docker secret create tls_cert ./cert.pem
# List secrets
docker secret ls
# docker-compose.yml (Swarm mode)
services:
api:
image: myapp-api
secrets:
- db_password
- api_key
environment:
# Code reads secrets from files, not env vars
- DB_PASSWORD_FILE=/run/secrets/db_password
- API_KEY_FILE=/run/secrets/api_key
secrets:
db_password:
external: true # created via `docker secret create`
api_key:
external: true
# Application code reads from file, not env var
import os
def get_secret(secret_name: str) -> str:
"""Read a secret from Docker Swarm secret file or environment variable."""
file_path = os.getenv(f"{secret_name}_FILE")
if file_path and os.path.exists(file_path):
with open(file_path) as f:
return f.read().strip()
return os.getenv(secret_name, "")
db_password = get_secret("DB_PASSWORD")
Audit your images for secrets
# Check build history for sensitive strings
docker history --no-trunc myimage | grep -i "password|token|secret|key"
# Use trivy to scan for secrets in images
docker run aquasec/trivy image --scanners secret myimage:latest
# Use truffleHog for secret scanning in git history
trufflehog git https://github.com/org/repo
The rules in summary
- Build-time secrets: use BuildKit
--mount=type=secretor--mount=type=ssh - Development runtime:
env_filepointing to a gitignored.env.local - Production runtime: Docker Swarm secrets or a secrets manager (Vault, AWS Secrets Manager)
- Never:
ARGfor secrets,ENVfor secrets,COPYfor secret files - Always: scan images before pushing to a registry
Secrets in images are one of the most common causes of credential leaks. The BuildKit secret mount takes thirty seconds to set up and eliminates the risk entirely at build time. There is no good reason not to use it.