Docker Compose Secrets Instead of Environment Variables
AI generated
FROM
RUN
Docker · Docker Compose · Secrets · Security
Docker Compose Secrets
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.

17 min read secrets · file · external · _FILE suffix Docker Compose 2.x

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.

11. FAQ: Docker Compose Secrets Instead of Environment Variables

1Why are secrets more secure than environment variables?
Secrets do not appear in docker inspect, environment variables do, in plain text.
2How do I define a file based secret?
Via the top level secrets key with file: path, then assign it in the service local secrets list.
3file vs. external secrets?
file reads locally, external references a secret already existing in the daemon.
4What does the _FILE suffix do?
The entrypoint reads the referenced file and sets the variable internally, without plain text visibility.
5Does the application need to be adapted?
Yes, unless it has native _FILE support, usually a simple file read is enough.
6Where are secrets stored?
Under /run/secrets/<name>, read only, present only for the container lifetime.
7How do I prevent accidental commits?
.gitignore for the secrets directory, a placeholder template, plus gitleaks or trufflehog in CI.
8Do secrets work without Swarm?
File based secrets yes, external secrets usually require Swarm.
9Can I rotate secrets without restarting?
No, docker compose up -d --force-recreate is needed to remount the file.
10Are .env files the same as secrets?
No, .env populates regular environment variables, just as visible as environment directly.