Skip to main content
RunBook Academy

Docker & ContainersIX Β· Docker ComposeCompose secrets

Secrets in Compose

Intermediate⏱ ~24 mindocker

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

Not yet marked complete on this device.

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 by docker 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 inspect prints it. Anyone in the docker group, 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>/environ exposes 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:

Read-only / Safethe _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:

  1. 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.
  2. A sync step on the host β€” a systemd unit, an agent sidecar, a vault agent template, a sops -d in the deploy script β€” materialises the current value into ./secrets/ with 0400 and the right owner, immediately before docker compose up.
  3. Compose mounts the file. The application is unchanged.
  4. 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

  1. Q1. A Compose `secrets:` entry with `file: ./db_password.txt` makes the value available to the service:

  2. Q2. In Compose on a single host, a file-sourced secret is stored encrypted and delivered to the container over TLS.

  3. Q3. Why is a mounted secret file safer than putting the same value in `environment:`? Select all that apply.

  4. Q4. Which secret sources does the Compose Specification actually define?

  5. 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.