Secrets in Docker: from --env-file to docker secrets

The fastest way to give a container a secret is an environment variable, and the fastest way to leak that secret is also an environment variable. Docker made env injection trivial — -e, --env-file, ENV in the Dockerfile — and the ecosystem has been paying for it ever since, with secrets showing up in image history, docker inspect output, and process listings. The fixes exist, but they are scattered across the Docker feature set, and nobody explains the progression.

Why environment variables are the wrong default

Anything set with -e at runtime is visible in docker inspect under Config.Env to anyone with Docker access. An ENV line in a Dockerfile is baked into the image configuration, so it survives in the registry and in every layer history. Build-time ARG values land in the image history too, which is how a secret ends up in a registry you thought was private. And once a secret is in the container’s environment, every child process inherits it — including the ones you did not intend.

--env-file is the first step up: it keeps the secret out of shell history and out of the Dockerfile. But the file itself sits on disk in plaintext, and the values still surface in docker inspect. It moves the problem; it does not solve it.

Build time: BuildKit secrets

The build stage was the worst offender until BuildKit’s RUN --mount=type=secret. This mounts a secret file during a single build step and never writes it into a layer:

RUN --mount=type=secret,id=api_key \
    ./build.sh

built with docker build --secret id=api_key,src=./api_key.txt. The secret is readable during that step and gone from the image afterward. If you build images that need credentials, this is the minimum bar.

Runtime: secret files over env

At runtime, the pattern that actually reduces exposure is files, not variables. Docker Swarm services get real secrets mounted under /run/secrets/, encrypted at rest with the swarm’s keys and delivered only to the services that requested them. Compose files support secrets: the same way for swarm deployments. The container reads /run/secrets/db_password instead of DB_PASSWORD.

Files beat env vars for two reasons. They do not show up in docker inspect, and they are not inherited by every child process. A process has to actively open the file, which means you can audit who opened it — and it gives you the option of a tool that mounts the secret, uses it, and closes it.

What still leaks regardless

Storage is only half the problem. The secret eventually becomes plaintext inside the container, and from there it can flow into logs, error output, and — in the agent era — into an LLM’s context window. No Docker feature scrubs that. The container platform gets the secret to the right place; keeping it out of output is a separate layer, applied at the process or transport boundary.

That layer is also your migration path off .env files. A tool like trustless will scan your directories for .env files, parse the KEY=VALUE pairs, back up the originals, and import them into a GPG-encrypted store — turning the files that --env-file reads into a one-time import instead of a permanent exposure.

The progression is real: inline -e, then --env-file, then BuildKit build secrets, then runtime secret files. Where in that progression are your containers — and what do your images’ history and docker inspect currently show?