Docker secrets management — keep credentials out of images
← Back
April 4, 2026Docker7 min read

Docker secrets management — keep credentials out of images

Published April 4, 20267 min read

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)

dockerfile
# 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:

dockerfile
# 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
bash
# 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

dockerfile
# syntax=docker/dockerfile:1
FROM node:20-slim

# Mount SSH agent for private repo access during npm install
RUN --mount=type=ssh npm ci
bash
# 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:

yaml
# 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
text
# .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:

bash
# 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
yaml
# 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
python
# 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

bash
# 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=secret or --mount=type=ssh
  • Development runtime: env_file pointing to a gitignored .env.local
  • Production runtime: Docker Swarm secrets or a secrets manager (Vault, AWS Secrets Manager)
  • Never: ARG for secrets, ENV for secrets, COPY for 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.

Share this
← All Posts7 min read