Docker & ContainersIX Β· Docker ComposeCompose secrets
Secrets in Compose
What you'll learn
- Inject secrets with the `file` and `environment` sources
- Explain precisely what Compose secrets do and do not protect against
- Contrast Compose secrets with Swarm secrets and with a real secret manager
- Audit a host for secrets that leaked into inspect output, logs or the image
Prerequisites
Verified against Docker Engine 29.x Β· Docker Engine 28.x Β· Docker Compose 2.x Β· containerd 2.x Β· runc 1.2.x Β· BuildKit 0.20+ Β· Linux kernel 5.15+ Β· Ubuntu 24.04 LTS Β· Debian 12 (Bookworm) Β· 2026-08-12
Compose secrets are mounted into the container at /run/secrets/<name> as
files. The container reads them like any other file. The Compose file
references the secret but does not contain its value.
That is a real improvement over an environment variable, for reasons worth being precise about β and it is a much smaller improvement than the word βsecretβ suggests.
The two sources
services:
api:
image: myorg/api:1.4.0
secrets:
- db_password
- source: api_token
target: token
uid: "1001"
gid: "1001"
mode: 0400
secrets:
db_password:
file: ./secrets/db_password.txt
api_token:
environment: API_TOKEN
file:β the secret is created from the contents of a file on the host. This is the source you will use.environment:β the secret is created from the value of an environment variable on the host at deploy time. Compose-only; it is not supported bydocker stack deploy.
Those are the only two sources the spec defines. There is no value: key
that lets you write a literal into the Compose file, and that omission is
deliberate.
The long syntax on the service side gives you source (the declared secret
name), target (the filename under /run/secrets, or an absolute path),
and uid, gid and mode for the mounted file. Without a target, the
file is named after the secret. Setting mode: 0400 and a uid matching
the containerβs user is how you stop every process in the container from
reading it β which matters more than it sounds, because a container often
runs a supervisor, a sidecar shell and your application under the same
namespace.
What actually happens on the host
Why this is still better than an environment variable
Given all of the above, it would be easy to conclude Compose secrets are theatre. They are not. An environment variable leaks in ways a file does not:
docker inspectprints it. Anyone in thedockergroup, and every monitoring agent that scrapes container metadata, gets the value.- Child processes inherit it. Every subprocess your application spawns carries the secret in its environment, including the ones that crash and write a dump.
/proc/<pid>/environexposes it to anything in the same PID namespace, and to root on the host.- Crash handlers and error trackers serialise it. Sentry, Rollbar and most language runtimes attach the environment to an unhandled exception by default. This is the most common real-world leak.
- It ends up in shell history and CI logs, because the way you set it is a command someone typed.
A file at /run/secrets/db_password with mode: 0400 and a matching uid
avoids every one of those. That is a genuine reduction in blast radius, and
it is the reason so many images support the _FILE convention:
# postgres, mysql, mariadb, redis and many others accept this form
# POSTGRES_PASSWORD_FILE=/run/secrets/db_password
# The entrypoint reads the file and never exports the value.
docker compose exec db sh -c 'ls -l /run/secrets/ && head -c 0 /run/secrets/db_password && echo readable'
docker compose exec db printenv POSTGRES_PASSWORD || echo 'OK: not in the environment'The second command failing is the verification. If POSTGRES_PASSWORD is
set in the environment as well as POSTGRES_PASSWORD_FILE, you have both
mechanisms and the weaker one wins for leakage purposes.
What not to do
# WRONG: there is no `value:` source, and if there were, this is the secret
# sitting in a file that is in git.
secrets:
db_password:
value: "REPLACE_ME"
# WRONG: visible in docker inspect, inherited by every child process,
# and attached to crash reports.
services:
api:
environment:
DB_PASSWORD: "REPLACE_ME"
# WRONG: baked into a layer. The layer is in the registry, and `docker
# history` reads it back out of any image built FROM this one.
ENV DB_PASSWORD=REPLACE_ME
The third is the worst because it is the least recoverable. Deleting the line and rebuilding does not remove the old layer from the registry, from any host that pulled it, or from any image built on top of it. A secret committed to an image layer is a secret that must be rotated, not deleted.
Real secret management, and where Compose fits
Compose secrets solve delivery into the container. They do not solve storage, rotation, audit, or revocation β the four things a secret manager exists for. The production pattern keeps Compose in the delivery role and puts something else behind it:
- The secret of record lives in a manager: Vault, AWS Secrets Manager, GCP Secret Manager, SOPS-encrypted files in git, whatever your organisation already runs.
- A sync step on the host β a systemd unit, an agent sidecar, a
vault agent template, asops -din the deploy script β materialises the current value into./secrets/with0400and the right owner, immediately beforedocker compose up. - Compose mounts the file. The application is unchanged.
- Rotation re-runs step 2 and restarts the consumers, because a long-running process that read the file at startup will not notice it changed.
secrets:
db_password:
external: true
name: prod_db_password
external: true declares that the secretβs lifecycle is managed outside
this file β Compose will not create it and will fail the up if it does not
exist. On a single host that means a docker secret from a Swarm-enabled
daemon; the value of the key here is mostly that it makes the dependency
explicit rather than implicit.
Knowledge check
Knowledge check Β· 5 questions
Q1. A Compose `secrets:` entry with `file: ./db_password.txt` makes the value available to the service:
Q2. In Compose on a single host, a file-sourced secret is stored encrypted and delivered to the container over TLS.
Q3. Why is a mounted secret file safer than putting the same value in `environment:`? Select all that apply.
Q4. Which secret sources does the Compose Specification actually define?
Q5. A credential was set with `ENV DB_PASSWORD=...` in a Dockerfile six months ago. The line has since been deleted and the image rebuilt. What is the correct remediation?
Passing score: 75%. Answers are checked in this browser.