How do you manage secrets securely with Docker?

7 minadvancedsecrets-managementsecurity

Quick Answer

Never bake secrets into an image via ARG, a COPY'd file, or a hardcoded ENV value — all of these persist in the image's layers/history and are readable by anyone who can access the image. Prefer runtime injection via -e/env_file sourced from a secrets manager (not committed to version control), Docker Swarm's or Compose's native secrets mechanism (which mounts secrets as files, not environment variables, reducing accidental exposure), or BuildKit's dedicated build-time secret mounting for anything a build step genuinely needs only transiently.

Detailed Answer

Why baking secrets into an image is always wrong

# NEVER do any of these
ENV DB_PASSWORD=supersecret
COPY .env /app/.env
ARG API_KEY
RUN curl -H "Authorization: Bearer $API_KEY" https://example.com

Every one of these persists the secret's value inside the image's layers or build history — recoverable by anyone with access to the image (docker history, inspecting layer contents directly, or simply docker run and reading the baked-in ENV/file). Recall from the layer-caching question that layers are effectively permanent once built — even a later layer that appears to "remove" the secret doesn't actually erase it from the earlier layer's stored data. This applies even to ARG (see that question) — build arguments can still leave traces in image metadata/history even though they're not automatically present in the running container's environment.

Runtime injection — better, but still has caveats

docker run -e DB_PASSWORD="$DB_PASSWORD" myapp:1.0

This avoids baking the secret into the image itself, but plain environment variables have their own real exposure risks. They're visible to anything that can inspect the container's configuration (docker inspect), and visible in process listings on some systems (/proc/<pid>/environ). They also commonly end up accidentally logged — many applications and frameworks log their full environment at startup for debugging purposes, inadvertently capturing secrets in log output — or exposed via a crash dump or error-reporting tool that includes environment context.

A better runtime mechanism: mounted secret files

# Docker Swarm's native secrets mechanism
echo "supersecret" | docker secret create db_password -
docker service create --secret db_password myapp:1.0
# Inside the container, the secret is available as a FILE, not an environment variable:
cat /run/secrets/db_password
# supersecret

Docker Swarm's native secrets mechanism, and Compose's own secrets: key (which can source from Swarm secrets or, for non-Swarm local development, a plain file), deliver a secret as a file mounted into the container at a well-known path, rather than as an environment variable. This avoids several of environment variables' specific exposure risks, such as accidental logging of the full environment or visibility in some process-inspection tools, since the secret only exists as file content the application must explicitly choose to read.

# Compose secrets (non-Swarm, file-based)
services:
  api:
    secrets:
      - db_password
secrets:
  db_password:
    file: ./db_password.txt    # this file itself must never be committed to version control

Build-time secrets — for values only needed transiently during the build

RUN --mount=type=secret,id=npm_token \
    NPM_TOKEN=$(cat /run/secrets/npm_token) npm install
docker build --secret id=npm_token,src=./npm_token.txt -t myapp .

BuildKit's dedicated secret-mounting syntax (--mount=type=secret) makes a secret available only during that specific RUN instruction's execution, without it persisting in any built layer or the final image's history at all. This is the correct mechanism for a private package registry token or similar credential needed transiently just to complete a build step, closing exactly the gap the ARG-for-secrets anti-pattern leaves open.

External secrets managers — the strongest option for production

For genuinely sensitive production secrets, integrate with a dedicated secrets manager — HashiCorp Vault, AWS Secrets Manager, and similar (see the SQL/Databases and Kubernetes stacks' equivalent questions). The application can fetch secrets directly at runtime, or a sidecar/init pattern can inject them. This provides stronger audit trails, rotation, and centralized access control than any Docker-native mechanism alone offers.

Secret needRight mechanism
Build-time only (e.g. a private registry token for npm install)BuildKit --mount=type=secret
Runtime, simple setupSwarm/Compose native secrets (file-based)
Runtime, needs audit trail/rotation/centralized controlExternal secrets manager (Vault, AWS Secrets Manager)
NeverARG, COPY, or ENV baked into the image