Secrets Handling
The naive Layer-A approach passed Driftwood's database password and SECRET_KEY as environment variables, read from the .env file from Chapter 4. That leaks. Environment values are visible in docker inspect, they bake into image layers visible through docker history, and they are inherited by every child process. Secrets belong in files mounted at run time — Docker and Compose secrets backed by tmpfs, or pulled from an external manager — never baked into an image and never committed to the repo.
This is where Driftwood's DB password stops being an environment variable and becomes a runtime secret. The app reads it from a file at /run/secrets/db_password instead of from DB_PASSWORD in its environment, and that one change pulls the credential out of inspect, out of the layers, and out of every child process's environment.
Why ENV and ARG Leak
Environment variables are not hidden. They show up verbatim in docker inspect on the container, they are inherited by every child process the app spawns, and they routinely land in crash logs and error reports. ARG and ENV values bake into image layers, so docker history reveals them too — the same lesson Chapter 4 topic 25 drew about configuration and the build-secret problem Chapter 5 solved. Anyone who can read the image or inspect the container reads the secret.
docker inspect and docker history, is inherited by every child process, and bakes into the image layers.tmpfs-backed file at /run/secrets/... the app reads — never in the environment, never in inspect, never on disk.Never Bake Secrets Into Images
A secret added in a Dockerfile survives in the image even if a later layer "deletes" it. The layered filesystem keeps the original write; a RUN rm only adds a whiteout on top (Chapter 2 topic 07), and the secret is still recoverable from the earlier layer through docker history and a layer dump. Pushing that image publishes the secret to anyone who can pull it, and rotating it means rebuilding the image. Secrets must never enter the build context — the one exception being BuildKit build secrets (Chapter 5), which mount during a single RUN and never persist in a layer.
Runtime Secrets as Mounted Files
Docker and Compose secrets deliver a value as a file at /run/secrets/<name>, backed by tmpfs so it lives in memory — never on the writable layer, never in the environment. The app reads the file instead of an environment variable, which is why most images that support this expose a _FILE convention: you set DB_PASSWORD_FILE=/run/secrets/db_password and the app reads the path. That is how driftwood/web gets its DB password.
services:
web:
image: driftwood/web
user: app
read_only: true
tmpfs:
- /tmp
environment:
# point the app at the file, not the value
DB_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
secrets:
db_password:
# the value lives in a file the repo never commits
file: ./secrets/db_password.txt
Compose mounts db_password at /run/secrets/db_password on a tmpfs, and the app reads it from there because DB_PASSWORD_FILE points at the path. The value is never in the environment, never in docker inspect, and never in the image — and the secrets/ directory is in .gitignore, so it never reaches a commit.
tmpfs Keeps Secrets Off Disk
Mounting the secret on a tmpfs (in-memory) filesystem means it is never written to the container's layers or to a durable volume, and it disappears when the container stops. A stolen disk image yields nothing, because the secret was only ever in RAM. This is the difference between a Docker/Compose secret and a plain bind-mounted file: the bind mount leaves the secret sitting unencrypted on the host filesystem, while the secret mount keeps it tmpfs-backed.
External Secret Managers
For rotation, auditing, and central control, the value should not live in a file on the host at all. It lives in a manager — HashiCorp Vault, AWS or GCP Secrets Manager — and is fetched at startup or injected as a file by an agent. The container holds only a short-lived credential, and the source of truth, with rotation policy and access logs, sits outside any image or host. For a single-host Driftwood the Compose secret is enough; the manager is where you go when you need rotation and an audit trail across many hosts.
- Passing the DB password as
-e DB_PASSWORD=...or in the.envfile — it shows indocker inspect, in child-process environments, and often in logs; the Layer-A.envis the naive version this topic replaces. - Baking a secret into the image via
ARG/ENVand assuming a laterRUN rmremoves it — it persists indocker historyand the earlier layer (Chapter 2 topic 07), and pushing the image publishes it. - Committing the
.envfile or a secrets file to the repo — version control keeps it forever in history even after deletion; secrets go in a manager or an ignored runtime file, never a commit. - Mounting a secret as a regular bind-mounted file on disk rather than
tmpfs— it then lives on the host filesystem unencrypted; use Docker/Compose secrets so it stays tmpfs-backed.
- Deliver runtime secrets as files via Docker/Compose
secretsand read them from/run/secrets/...(the_FILEconvention), not as environment variables, so they stay out of the environment and out ofinspect. - Keep secrets tmpfs-backed and out of the image and the writable layer, so neither
docker historynor a stolen disk image exposes them. - Use BuildKit
--secretmounts (Chapter 5) for any secret needed only at build time, so it never lands in a layer. - Source production secrets from an external manager (Vault, a cloud secret store) for rotation and audit, handing the container only a short-lived credential.
Secret objects fill the runtime-file role in a cluster
gitleaks · git-secrets catch secrets before they reach a commit
Knowledge Check
Why is passing a secret as an environment variable a leak?
- It is visible in
docker inspect, inherited by child processes, and baked into layers seen viadocker history - Environment variables are encrypted at rest, but the matching decryption key ships inside the very same image
- They make the container start slowly enough that an attacker can race in and read the value from process memory
- The kernel logs every environment variable to the host syslog by default, where any local user can grep it out
A secret was added in a Dockerfile and removed with a later RUN rm. Is it gone?
- No — the original write survives in the earlier layer; the
rmonly adds a whiteout, so it is still recoverable - Yes — the
rmreaches back and deletes the file from every earlier layer, so the secret is fully and permanently removed - Yes — Docker automatically squashes all the layers together on every build, quietly erasing the deleted secret
- No, but pushing the image to a remote registry strips the secret out of the layers automatically on upload
How does a Docker/Compose secret reach the application?
- As a tmpfs-backed file at
/run/secrets/<name>that the app reads, often via a_FILEenvironment variable - As an environment variable injected only at runtime, late enough that
docker inspectcan no longer see its value - Baked into a dedicated encrypted image layer that the app reads and decrypts in memory at container startup
- Fetched fresh over the network from the Docker daemon's secret store on every single read the app performs
Why mount the secret on tmpfs rather than as a plain bind-mounted file?
- A tmpfs keeps the secret in memory only, so it never lands on disk and a stolen disk image yields nothing
- A bind-mounted file is perfectly fine because the host filesystem transparently encrypts the secret at rest for you
- A tmpfs simply lets the app read the secret measurably faster than it could from a regular on-disk file
- A tmpfs makes the secret reliably persist across container restarts and reboots purely for operator convenience
You got correct