Docker Secrets Management: What Actually Works
Here's the short version of Docker secrets management: environment variables are the wrong place for passwords and API keys, because they leak through docker inspect, logs, and child processes. For runtime secrets, use Docker Swarm secrets or Compose file-based secrets, which mount credentials as read-only files under /run/secrets/. For build-time secrets, use BuildKit's --mount=type=secret. And for anything at real scale, hand the job to a dedicated store like HashiCorp Vault or AWS Secrets Manager.
That covers 95% of cases. The rest of this post explains why, with commands you can actually run.
Why environment variables leak
Everyone starts here:
docker run -e DB_PASSWORD=hunter2 myapp
It works, so people ship it. The problem is that an env var set this way is visible to anyone who can talk to the Docker daemon:
docker inspect myapp | grep -A5 '"Env"'
That prints DB_PASSWORD=hunter2 in plain text. So does the output of docker inspect in your CI logs, your monitoring agent, and any crash reporter that dumps the environment. Env vars are also inherited by every child process the container spawns, and they frequently end up in application logs the first time someone writes a lazy debug line like console.log(process.env).
Using an .env file with Compose is the same problem wearing a coat. The value still lands in the container's environment and still shows up in docker inspect. It keeps the secret out of your shell history, which is a real win, but it does nothing about runtime exposure.
So env vars are fine for config (log level, feature flags, the region you're in). They are not fine for credentials.
Docker Swarm secrets: the built-in answer
If you run Swarm, Docker has a real secrets primitive. Secrets are stored encrypted in the Raft log, sent to worker nodes over mutual TLS, and mounted into the container as an in-memory file, never written to the node's disk.
Create one from a file or stdin:
# from stdin (nothing touches disk, note the trailing dash)
printf 'hunter2' | docker secret create db_password -
# or from a file, then shred the file
docker secret create db_password ./db_password.txt
shred -u ./db_password.txt
Attach it to a service:
docker service create \
--name myapp \
--secret db_password \
myapp
Inside the container the value lives at /run/secrets/db_password. Your app reads it like any file:
with open("/run/secrets/db_password") as f:
db_password = f.read().strip()
That file is on a tmpfs mount, owned by root and mode 0444 by default, and it disappears when the task stops. It never shows up in docker inspect on the service, and it never lands on the worker's filesystem. You can also rename where it mounts and tighten permissions:
docker service create --name myapp \
--secret source=db_password,target=db_pass,mode=0400,uid=1000 \
myapp
Rotation is the one rough edge. Docker secrets are immutable, so you rotate by creating a new one and updating the service to swap it in:
printf 'newpassword' | docker secret create db_password_v2 -
docker service update \
--secret-rm db_password \
--secret-add source=db_password_v2,target=db_password \
myapp
That triggers a rolling restart, because the app has to re-read the file. Plan for it.
Compose file secrets on a single host
Most people aren't running Swarm. You don't have to. Compose supports file-based secrets without a swarm, and it gives you the same /run/secrets/ mount pattern:
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txt
Run it with plain docker compose up. The file gets mounted read-only inside the container, and the value never enters the environment or docker inspect output. Note the _FILE convention: the official Postgres, MySQL, and many other images accept *_FILE variants of their env vars precisely so you can point them at a mounted secret instead of passing the value inline. Use those when the image supports them.
Keep the secrets/ directory out of git. Add it to .gitignore on day one, before you forget. A leaked credential in your history is a bad afternoon, and rewriting history on a shared repo is worse.
Build-time secrets: don't bake them into layers
A classic mistake is needing a private token during docker build, so you do this:
# WRONG: the token is now in an image layer forever
ARG NPM_TOKEN
RUN echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc && npm ci
Even if you delete the file in a later step, the token is still recoverable from the intermediate layer. Anyone who pulls the image can extract it with docker history and a bit of digging.
BuildKit fixes this with secret mounts. The secret is available only during that one RUN step and is never written to any layer:
# syntax=docker/dockerfile:1
FROM node:20
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
npm ci
DOCKER_BUILDKIT=1 docker build \
--secret id=npmrc,src=$HOME/.npmrc \
-t myapp .
The .npmrc exists inside the build only while npm ci runs, then it's gone. No layer, no docker history leak. For SSH-based git dependencies during build, --mount=type=ssh does the same thing for your agent socket.
When to reach for a dedicated secret store
Built-in secrets are great for a handful of services on one or two hosts. Once you have many services, multiple environments, audit requirements, or a compliance team asking questions, a purpose-built store earns its keep. HashiCorp Vault, AWS Secrets Manager, and Google Secret Manager all give you things Docker's primitives don't: dynamic short-lived credentials, per-request audit logs, fine-grained access policies, and automatic rotation.
The usual pattern is that your container authenticates to the store at startup (via an IAM role, a Kubernetes service account, or an AppRole) and pulls what it needs into memory. Nothing sensitive lives in the image or the Compose file. The tradeoff is real operational weight: you now run and secure the store itself, and a Vault outage can mean apps can't start. Don't adopt one because a blog post told you to. Adopt one when you have a concrete reason, like needing to rotate a database credential every hour or prove who read a key and when.
Whichever route you pick, secrets are one layer of a bigger picture. Pair this with sane image hygiene from Docker security best practices for production, and scan those images before they ship using one of the Docker security scanning tools compared.
The part everyone forgets: the SSH key to the host
All of the above protects secrets inside your containers. But the credential that unlocks the whole box is the SSH key to the host running Docker. If that key sits unprotected on a laptop or, worse, a phone, none of your /run/secrets/ hygiene matters. Whoever has the key runs docker exec and reads every mounted secret directly. Treat host access with the same seriousness, and if you manage servers from a phone, read how to protect SSH keys on a mobile device before you store one there.
This is where Docker HQ fits for us. It keeps SSH keys in the device secure enclave and lets you inspect running containers, read logs, and open a shell over SSH from your phone without ever dumping a secret into an env var you can see in a screenshot. When a host or a container goes down at 3am, you get a push alert instead of finding out from a customer. If you're evaluating it for a team, the pricing page lays out the RBAC and multi-host access.
Docker secrets management FAQ
Are Docker secrets encrypted at rest?
In Swarm, yes. Secrets are stored encrypted in the Raft log on manager nodes and transmitted to workers over mutual TLS. On a worker, the secret is mounted into the container on an in-memory tmpfs, so it never touches the node's disk. With plain Compose file secrets there is no encryption layer: the file sits on your host filesystem, so protect that file with normal permissions and keep it out of git.
Can I use Docker secrets without Swarm?
Yes, with Docker Compose. Define a secrets: block backed by a file: and reference it from your service. Compose mounts it under /run/secrets/ exactly like Swarm does, no docker swarm init required. You lose Swarm's encrypted storage and rotation tooling, but you get the file-based pattern that keeps values out of the environment.
Why not just use an .env file?
An .env file keeps secrets out of your shell history and your compose file, which is worth something. But the values still get injected into the container's environment, which means they show up in docker inspect, get inherited by child processes, and can leak into logs. Use .env for non-sensitive config, and use file-based secrets for actual credentials.
How do I rotate a Docker secret?
Docker secrets are immutable, so you create a new one and swap it in. In Swarm, run docker secret create db_password_v2 -, then docker service update --secret-rm db_password --secret-add source=db_password_v2,target=db_password myapp. That forces a rolling restart so the app re-reads the file. If rotation needs to be frequent or zero-downtime, that friction is your signal to move to Vault or a cloud secret manager.
Where to start
If you're on a single host today, the fastest safe win is converting your -e PASSWORD=... flags to Compose file secrets with the *_FILE env convention. It takes ten minutes and closes the most common leak. Grep your repo for docker inspect in CI scripts while you're at it, that's usually where secrets quietly end up in the logs.