Docker secrets, and why most teams ship them anyway

Posted on Sep 12, 2024

I’ve seen this happen more times than I’d like to admit: someone commits a .env file, or bakes an API key directly into a Dockerfile, and it ends up in the final image. Sometimes it’s in git history. Sometimes it’s in the layer cache. Often both.

Why it keeps happening

The problem isn’t that developers are careless. The problem is that the path of least resistance is almost always the insecure one.

Docker’s build context makes it trivially easy to COPY . . and accidentally include secrets. .dockerignore exists but nobody sets it up properly at project start, and by the time someone notices, the pattern is already entrenched.

What actually works

Multi-stage builds. Keep secrets out of the final image by doing secret-dependent steps in a builder stage and only copying the output.

BuildKit --secret flag. Available since Docker 18.09. Mounts a secret at build time without it ever appearing in the image layers.

# syntax=docker/dockerfile:1
RUN --mount=type=secret,id=api_key \
    API_KEY=$(cat /run/secrets/api_key) ./build.sh

Vault or a secrets manager. At runtime, fetch secrets from the environment rather than baking them in. More moving parts, but the right answer at scale.

What doesn’t work

  • Hoping people will remember
  • .gitignore alone (history is forever)
  • Rotating keys after the fact without auditing every place they were used

The last point matters more than people think. If a key was in an image, it was in a registry, possibly cached by CI, possibly pulled by a dozen environments. Rotation isn’t enough unless you also know the blast radius.


None of this is new. It’s just consistently ignored until something goes wrong.