A Compose file often starts harmlessly: one database, one application container, and one sensitive value copied into an environment block so the stack boots. Six months later the same file has backups, workers, a metrics sidecar, and several places where the same value can leak into logs or shell history. That is a reasonable shortcut for a toy service, but it ages badly.
The practical question is not whether every small Docker stack needs a full vault. It is narrower: when a Compose service only needs a database login or API credential, should that value live in the container environment, or should it be mounted as a secret file and granted only to the services that need it? For new Compose work, Docker’s own documentation points toward secrets for sensitive values, not plain environment variables.
On this page
Why environment variables are a poor place for credentials#
Docker’s Compose environment-variable documentation gives a direct warning: do not use environment variables to pass sensitive information such as passwords into containers; use secrets instead. The same page explains that environment and env_file both set values inside the container environment, even if they make the Compose file cleaner.
That distinction matters. Moving a database login from the main Compose file into a separate environment file may reduce visual clutter, but the value still becomes part of the container environment. Docker’s Compose secrets guide calls out the risk: environment variables are often available to all processes, can be hard to track, and may be printed during debugging without you noticing.
Environment variables are still fine for non-sensitive configuration: feature flags, hostnames, log levels, cache sizes, and similar values. The boundary is secrecy. If disclosure would require rotation, notification, or incident response, treat it as a secret rather than a setting.
How Compose secrets reach only the services you grant#
Compose secrets use two parts. First, a top-level secrets section defines where the value comes from. Second, each service that needs the value lists that secret under its own secrets attribute. Docker’s guide says Compose mounts the secret in the container at /run/secrets/<secret_name>, and that services only receive secrets when explicitly granted.
The Compose file secrets reference documents two source types: file, where the secret is created from a file’s contents, and environment, where the secret is created from a host environment variable. It also notes an important portability limit: environment-sourced secrets are supported by Docker Compose, not by docker stack deploy.
There is one more platform limit worth naming before you design around this pattern. Docker’s secrets guide says Compose secrets are supported for Linux containers only; the implementation bind-mounts a single file into the container, while Windows containers support bind-mounting directories only. For Linux-based homelab and small production stacks, though, that file-based model is often enough to remove sensitive values from the normal process environment.
A safer migration pattern for a database-backed stack#
The weak pattern is not complicated: a service mixes ordinary settings with sensitive settings in the same environment block. A safer first step is to leave harmless configuration in the environment and grant the sensitive value as a separate secret file.
services:
api:
image: example/webapp
environment:
APP_MODE: production
DATABASE_HOST: db
secrets:
- app_database_login
secrets:
app_database_login:
file: ./secrets/app_database_login.txt
That example does not assume a magic variable name inside your application. Some Docker Official Images support file-path conventions for selected settings; Docker’s secrets guide gives MySQL and Postgres as examples of images that use that style. For a custom application, add explicit file-reading support and keep the file path in a non-sensitive setting or a default configuration path.
The source file still needs careful handling: keep it outside version control, restrict local file permissions, back it up only in encrypted stores, and avoid copying it into issue trackers or chat. If the old value ever lived in Git, treat the work as a rotation, not a cosmetic cleanup. The related Orthogonal guide on catching leaked secrets before commit is the right companion task.
Where Compose secrets stop helping#
Compose secrets reduce one specific class of exposure: sensitive values sitting in environment variables for every process in a container to inherit or print. They do not replace a secret-management service with policy, approvals, history, and automatic rotation. OWASP’s Secrets Management Cheat Sheet recommends least privilege, automation where possible, rotation, revocation, expiration, and audit records for secret access.
That means Compose secrets are a good local delivery mechanism, not a full lifecycle. They are useful when you are running a few containers on one host and want better isolation than environment variables. They are not enough when several teams need delegated access, when every read must be audited, or when credentials should be minted dynamically and expire after a short session.
Also be careful with shared secrets. If the same database login is mounted into a web app, a worker, a migration job, and a backup container, every one of those services becomes part of the exposure area. Compose’s per-service grants help you see that list in code. Use that visibility to remove consumers that do not need the value anymore.
Make rotation boring, not heroic#
The hardest part of secret cleanup is usually not writing the YAML. It is agreeing on what happens when the value changes. OWASP describes a secret lifecycle that includes creation, rotation, revocation, and expiration, and it calls for audit records such as who requested a secret, what system or role used it, when it expired, and whether there were authorization errors.
For a Compose stack, turn that into a small runbook. Generate the new value outside the repository. Place it in the secret source file or the upstream secret store that writes that file. Restart only the services that need the value during a planned window. Revoke the old credential at the database, API provider, or identity system. Record the date, the affected services, and the place where the old value was removed.
That runbook should sit next to your deployment notes, not inside the secret file. Pair it with image and dependency scanning as a separate control; the Orthogonal post on running Trivy across homelab containers covers that adjacent cleanup path.
Run this audit before editing a live Compose stack#
Start small. Open one Compose project and review the service environment and env_file entries for values whose disclosure would force rotation. Leave non-secret configuration alone. Pick one credential that has a clear owner and a known rotation path.
Move that one value into a top-level Compose secret, grant it only to the service that reads it, and update the application or image setting to read from the mounted file. Then rotate the credential so the old environment-variable value is no longer useful. Do one service first; a boring, reviewed migration beats a wide edit that leaves nobody sure which value is active.
Leave a Reply