Instead of passwords in environment variables
Credentials in the environment section of a compose.yaml end up in docker inspect, in process lists and often accidentally in log output. The secrets block in Docker Compose solves this problem by delivering credentials as files inside the container instead of as visible environment variables, creating a significantly cleaner way of handling passwords and API keys.
Table of Contents
- 1. Why environment variables are problematic for credentials
- 2. The secrets block in Docker Compose: the basics
- 3. File based secrets: securing local development
- 4. External secrets: using already existing Docker secrets
- 5. The _FILE suffix: making applications secret capable without changes
- 6. Practical example: MySQL root password as a secret
- 7. Practical example: PHP and Magento with secrets for API keys
- 8. Keeping secrets out of version control
- 9. Secrets compared to environment variables and .env files
- 10. Summary
- 11. FAQ
1. Why environment variables are problematic for credentials
In many compose.yaml files, database passwords, API keys and other credentials end up directly in the environment section. That works technically, but is problematic from a security perspective, because environment variables are visible in several places that are rarely considered. With docker inspect, anyone with access to the Docker daemon can read the complete environment variables of a running container, including all passwords in plain text.
In addition, environment variables often end up in process lists, for example when an application logs them at startup, or in crash dumps that get shared with third parties for troubleshooting. Many logging frameworks also accidentally dump the entire environment at startup when a debug mode is active. Docker Compose secrets solve this problem structurally by delivering credentials not as an environment variable, but as a file in the container's filesystem, typically under /run/secrets/.
The decisive difference: a file under /run/secrets/ is not automatically shown by docker inspect and does not end up in the process environment that many monitoring and logging tools capture by default. Anyone who consistently uses Docker Compose secrets for all credentials significantly reduces the attack surface, without needing to fundamentally rebuild the application itself.
2. The secrets block in Docker Compose: the basics
Docker Compose has two levels for secrets: the top level secrets definition, which specifies where a secret comes from, and the service local secrets list, which specifies which secrets a particular service actually receives. This separation allows a secret to be defined once and made available specifically only to the services that actually need it, instead of injecting it into every container by default.
Each secret is mounted at container start as a file under /run/secrets/<secret-name>, with the file contents being the actual secret value. This file is read only and by default only readable for the root user inside the container, which offers additional isolation from other processes in the same container network compared to an environment variable visible to all processes.
# compose.yaml — basic secrets block
services:
api:
image: myapp/api:latest
secrets:
- db_password
- api_key
secrets:
db_password:
file: ./secrets/db_password.txt
api_key:
file: ./secrets/api_key.txt
Inside the container, the content of db_password.txt is then available under /run/secrets/db_password. The application reads the file at startup instead of querying an environment variable. This pattern works with practically any programming language, because file access is a basic feature that requires no additional library.
3. File based secrets: securing local development
The simplest form of Docker Compose secrets is the file based variant, where a local file serves as the source. For local development this means: credentials live as individual text files in a secrets directory that is explicitly excluded from version control, while a secrets.example template with placeholders is checked in, so new team members know which secrets are needed.
A common use case is separating development and production values. In the local compose.yaml, the file entries point to harmless development values, while in production like environments the same structure is filled with real values managed through a secret management system. The application itself does not need to know whether it is working with development or production values, because it always reads the same file path under /run/secrets/.
# Set up local secrets directory (excluded from version control)
mkdir -p secrets
echo -n "dev-only-password" > secrets/db_password.txt
echo -n "dev-api-key-12345" > secrets/api_key.txt
# Restrict permissions so only the owner can read the files
chmod 600 secrets/*.txt
# Verify the secret is visible inside the running container
docker compose exec api cat /run/secrets/db_password
4. External secrets: using already existing Docker secrets
Besides file based secrets, Docker Compose also supports external secrets that already exist outside the compose.yaml, for example as a Docker Swarm secret or through an externally managed secret management system. With external: true, Compose points to a secret that is already registered in the Docker daemon, instead of creating it new from a local file. This is especially relevant when secrets are managed centrally and shared across multiple Compose projects.
In a local Docker Desktop or Docker Engine installation without Swarm mode, external secrets can be created via docker secret create, provided Swarm mode is enabled. For pure Docker Compose setups without Swarm, the file based variant is usually more practical, because it requires no additional Swarm initialization and works directly with a simple docker compose up.
# compose.yaml — external secret managed outside this project
services:
api:
image: myapp/api:latest
secrets:
- source: db_password
target: db_password
mode: 0400
secrets:
db_password:
external: true
name: production_db_password
5. The _FILE suffix: making applications secret capable without changes
Not every application reads configuration values from a file. Many frameworks and libraries still expect a classic environment variable. An established pattern supported by many official Docker images, for example the official MySQL and Postgres images, is the _FILE suffix. Instead of setting DB_PASSWORD directly, DB_PASSWORD_FILE is set with the path to the secret file, and the image's entrypoint reads the file itself and sets the actual variable internally.
This pattern combines the security of secrets with the simplicity of environment variables, without the application itself needing to be adapted, as long as it uses an image that supports the _FILE suffix. For custom applications that do not bring this mechanism, it is easy to replicate in the entrypoint script by checking whether a _FILE variable is set, and if so, loading its contents into the actual variable.
# compose.yaml — using the _FILE suffix pattern with official images
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD_FILE: /run/secrets/mysql_root_password
MYSQL_DATABASE: magento
secrets:
- mysql_root_password
secrets:
mysql_root_password:
file: ./secrets/mysql_root_password.txt
6. Practical example: MySQL root password as a secret
The official MySQL image natively supports the _FILE suffix for MYSQL_ROOT_PASSWORD, MYSQL_PASSWORD and several other variables. This makes it possible to build a production like local setup where the root password never appears as a plain text environment variable in the container, but exclusively as file content under /run/secrets/. The MySQL entrypoint reads the file on first startup and initializes the database accordingly.
For Magento projects that often need multiple database users for different purposes, for example an application user with restricted rights alongside the root user, the same pattern can be repeated for every additional user. Each password gets its own secret file, and the application itself reads its credentials through the same _FILE mechanism or directly from /run/secrets/, depending on what the database framework used supports.
7. Practical example: PHP and Magento with secrets for API keys
For PHP applications that do not have native _FILE support, reading a secret is a simple file operation. Instead of calling getenv("PAYMENT_API_KEY"), the application reads file_get_contents("/run/secrets/payment_api_key") and uses the trimmed content as the value. In Magento, this pattern can be encapsulated in env.php or in a custom configuration provider, so the rest of the application still accesses a configuration value in the normal way, without knowing the details of the secrets implementation.
A practical advantage of this approach: secrets can be rotated at runtime by swapping out the underlying file, without needing to restart the container, provided the application rereads the file on every access instead of caching it once. For credentials that rarely change, caching on first read is usually sufficient, for more frequently rotated values a periodic reread is worthwhile.
# Read a secret from within a running PHP container for debugging
docker compose exec php-fpm cat /run/secrets/payment_api_key
# Verify file permissions on the mounted secret
docker compose exec php-fpm stat /run/secrets/payment_api_key
# Rotate a secret without restarting: replace the source file, then
# re-run compose up to remount it into a freshly created container
docker compose up -d --force-recreate php-fpm
8. Keeping secrets out of version control
A secret that gets accidentally checked into a Git repository is effectively compromised, even if the commit is later reverted, because the Git history still contains the value. For file based Docker Compose secrets that means: the secrets directory with the actual values consistently belongs in .gitignore, while a secrets template with placeholders or example values is checked in, so the project setup remains documented for new team members.
In addition, a pre commit hook or a CI step that checks whether real secret files were accidentally checked in is recommended. Tools like gitleaks or trufflehog scan a repository for typical patterns of passwords, API keys and certificates and raise an alarm before a secret actually enters the history. This combination of .gitignore and automated scanning is the practical complement to the technical protection provided by Docker Compose secrets.
9. Secrets compared to environment variables and .env files
For credentials there are several approaches in Docker Compose that look similar at first glance, but differ significantly in security and visibility. The choice between them should be made deliberately, instead of always using the same variant out of habit.
| Method | Visible in docker inspect | In process environment | Recommendation for credentials |
|---|---|---|---|
| environment directly | Yes, in plain text | Yes | Not suitable for passwords |
| .env file | Yes, becomes environment | Yes | Only for non critical configuration |
| Docker Compose secrets | No | No | Recommended for passwords and API keys |
| External secret management | No | No | Recommended for production multi team environments |
The difference between file based secrets and a full external secret management system lies mainly in rotation convenience and centralized access control. For local development and smaller deployments, the file based secrets block is entirely sufficient, while larger organizations with many teams benefit from a central system that can reference Docker Compose secrets as an external source.
Mironsoft
Secure Docker infrastructure and secrets management
Passwords that no longer show up in docker inspect?
We migrate existing Docker Compose stacks from plain text environment variables to a clean secrets workflow, including .gitignore protection and a rotation concept.
Secrets audit
Reviewing existing compose.yaml files for plain text credentials in environment
Migration
Switching to file based or external secrets without application downtime
Magento secrets
Safely wiring database credentials and payment API keys into env.php
10. Summary
Docker Compose secrets solve a structural security problem of classic environment variables: credentials that become visible in docker inspect, process lists and often in logs. With the secrets block, passwords and API keys are instead delivered as files under /run/secrets/, either file based for local development or as externally referenced secrets from a central management system. The _FILE suffix allows many official images to be used in a secret capable way without any code changes.
For PHP and Magento projects, the same mechanism can be replicated with a simple file read operation if the application itself does not have native secrets support. Combined with a consistent .gitignore strategy and automated secret scanning, this creates a setup where credentials end up neither in version control nor in easily readable environment variables.
Docker Compose Secrets — The Essentials at a Glance
File based
Local file as the secret source, ideal for development environments, excluded from version control.
External
References a secret already present in the Docker daemon, suitable for centrally managed credentials.
_FILE suffix
Many official images automatically read a secret file when the variable name ends in _FILE.
Not in docker inspect
Secrets do not appear in the visible container environment, unlike environment variables.