Managing Secrets and Sensitive Configuration

Intermediate
11 min

Managing Secrets and Sensitive Configuration

Environment variables are convenient for configuration, but they are a poor home for passwords, API tokens and private keys. This lesson explains where secrets leak in a typical Docker workflow, how to keep them out of images with BuildKit secret mounts, how to deliver them to running containers as files with Compose and Swarm secrets, and how to structure applications so both approaches work.

Where Secrets Leak

Every one of these puts a secret somewhere it will be found later:

| Approach | Why it leaks | |----------|--------------| | COPY .env . or COPY .npmrc . | The file is in a layer forever, even if a later RUN rm deletes it | | ARG API_TOKEN | Recorded in the image metadata; docker history shows it | | ENV DB_PASSWORD=... | Visible in docker inspect and to every process in the container | | -e DB_PASSWORD=... on the command line | Stored in shell history and visible in docker inspect | | Committing compose.yaml with inline values | Ends up in Git history |

bash
docker history --no-trunc my-app | grep -i token # reveals ARG and ENV values docker inspect --format '{{.Config.Env}}' api # reveals run-time -e values

Environment variables are acceptable for non-sensitive settings and, with care, for secrets injected at run time by a platform. They are never acceptable inside an image.

Build-Time Secrets with BuildKit

When a build needs a private registry token, an SSH key or a .npmrc, mount it for a single RUN step. The file exists only during that command and is not stored in any layer or in the cache:

dockerfile
# syntax=docker/dockerfile:1 RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci RUN --mount=type=secret,id=pip,target=/etc/pip.conf pip install -r requirements.txt RUN --mount=type=ssh git clone git@github.com:org/private-lib.git
bash
docker build --secret id=npmrc,src=$HOME/.npmrc -t my-app . docker build --secret id=pip,env=PIP_CONF -t my-app . # take the value from an env var docker build --ssh default -t my-app . # forward the local SSH agent

The id links the Dockerfile mount to the --secret flag; target sets the path inside the build. In Compose, the same is expressed under build.secrets.

Run-Time Secrets as Files

Compose can mount a secret into a container at /run/secrets/<name>. The source can be a local file or an environment variable on the machine running Compose:

yaml
services: db: image: postgres:17 environment: POSTGRES_PASSWORD_FILE: /run/secrets/db_password secrets: - db_password api: build: . secrets: - db_password - source: stripe_key target: /run/secrets/stripe mode: 0400 secrets: db_password: file: ./secrets/db_password.txt stripe_key: environment: STRIPE_KEY

The secrets/ directory belongs in .gitignore and .dockerignore. In Docker Swarm the same secrets: key refers to secrets stored encrypted in the cluster (docker secret create db_password -), and Kubernetes has an equivalent Secret object, so an application that reads secrets from files ports cleanly between all three.

The _FILE Convention

Many official images accept a variable suffixed with _FILE that points to a file containing the value: POSTGRES_PASSWORD_FILE, MYSQL_ROOT_PASSWORD_FILE, MONGO_INITDB_ROOT_PASSWORD_FILE. Adopt the same pattern in your own code so it works with both plain variables in development and mounted files in production:

javascript
import { readFileSync } from "node:fs"; function secret(name) { const file = process.env[`${name}_FILE`]; if (file) return readFileSync(file, "utf8").trim(); return process.env[name]; } const dbPassword = secret("DB_PASSWORD");

Practical Rules

  • Keep an .env.example with placeholder values in Git, and the real .env out of it; Compose reads .env automatically for variable interpolation.
  • Pass secrets from CI as masked variables and forward them with --secret id=...,env=VAR, never by writing them into the Dockerfile.
  • Rotate any credential that has ever been committed or baked into an image; removing it from the latest tag does not remove it from history.
  • For production fleets, a secret manager (HashiCorp Vault or a cloud provider's secrets service) injects values at start-up and handles rotation.
Quick Quiz
Question 1 of 3

Why does `COPY .env .` followed by `RUN rm .env` still leak the file?

Key Takeaways

  • COPY, ARG and ENV all leave secrets recoverable from the image; environment variables at run time are visible in docker inspect.
  • Use RUN --mount=type=secret with docker build --secret for build-time credentials and --ssh for private Git.
  • Compose secrets: mounts files under /run/secrets/; the same key works with Swarm's encrypted store.
  • Support the _FILE convention in your application so it reads secrets from files or variables.
  • Keep .env out of Git and out of the build context, and rotate anything that was ever committed.

Next lesson: Docker Compose for Multi-Container Apps — define and run an entire application stack with one file.

Managing Secrets and Sensitive Configuration - Docker | CodeYourCraft | CodeYourCraft