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?