Introduction to Docker Compose Secrets
Handling sensitive data like API keys, database passwords, and TLS certificates is one of the trickiest aspects of containerized development. Docker Compose offers a dedicated mechanism for managing secrets that keeps them out of your images, environment variables, and version control. This tutorial walks you through what secrets are, why they matter, how to use them effectively, common mistakes to avoid, and battle-tested best practices for both local development and production.
What Are Secrets in Docker Compose?
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →
Secrets in Docker Compose are a secure way to provide sensitive configuration values to containers without embedding them directly in the Compose file, environment variables, or the container image. When you define a secret, Docker makes it available to the service as a file mounted at /run/secrets/<secret_name>. The container can read the secret from that file, but the secret never appears in docker inspect, shell histories, or environment variable dumps.
Built-in Secret Management vs. File-Based Mounts
It's important to distinguish between two modes of operation:
- Docker Swarm secrets – encrypted at rest, transmitted securely over mutually authenticated TLS, and only decrypted on the target node. Requires a Swarm cluster.
- Compose file secrets (local development) – simply bind-mounts a file from the host into
/run/secrets/. No encryption at rest, but still prevents accidental exposure through environment variables or image layers.
This tutorial covers both modes, with a focus on practical Compose usage and migration paths to production-grade Swarm secrets.
Why Secrets Matter
The primary reason to use secrets is defense in depth. Secrets help you:
- Avoid leaking credentials through
docker inspector container logs. - Prevent sensitive values from being baked into container images (where they persist in layers forever).
- Decouple secret lifecycle from application configuration.
- Make accidental commits of secrets to version control much harder.
- Prepare your local development workflow for Swarm-based production deployments.
Without secrets, developers often resort to environment variables, which can be trivially exposed by a simple env command inside the container or by printing debugging information. Secrets provide a file-based interface that applications can read once at startup and then clear from memory, reducing the attack surface.
How to Use Secrets in Docker Compose
Declaring Secrets in the Compose File
Secrets are defined under a top-level secrets key, and then referenced by services that need them. Here is a basic example for local development:
version: '3.8'
services:
web:
image: nginx:latest
secrets:
- db_password
environment:
- DB_PASSWORD_FILE=/run/secrets/db_password
secrets:
db_password:
file: ./secrets/db_password.txt
In this setup, the file ./secrets/db_password.txt on the host is mounted into the container at /run/secrets/db_password. The application can read the file at that path instead of relying on an environment variable.
Mounting Secrets into Services
You can control the target path and permissions with more detailed syntax:
version: '3.8'
services:
api:
image: myapp:latest
secrets:
- source: db_password
target: /custom/path/password
mode: 0400
- api_key
secrets:
db_password:
file: ./secrets/db_password.txt
api_key:
file: ./secrets/api_key.txt
The target changes the mount point inside the container, and mode sets file permissions (octal). Always restrict permissions to read-only for the owner (0400) whenever possible.
Using Secrets in Application Code
Your application should read secrets from files. A common pattern in shell scripts or entrypoints is:
#!/bin/sh
# Read secret and export as environment variable for legacy apps
export DB_PASSWORD=$(cat /run/secrets/db_password)
# Then start the main application
exec "$@"
For a Node.js application, you might do:
const fs = require('fs');
const dbPassword = fs.readFileSync('/run/secrets/db_password', 'utf8').trim();
The key principle is: never treat secrets as environment variables directly in the Compose file. Use the secret file and read it inside the container.
Environment Variable Fallacy
A common shortcut is to define secrets via environment variables in the Compose file:
services:
web:
environment:
- DB_PASSWORD=supersecret # DON'T DO THIS
This is insecure because the value is visible in docker inspect, appears in container environment dumps, and can be leaked in log output or crash reports. Always use the file-based secrets mechanism instead.
Common Pitfalls
1. Embedding Secrets Directly in Environment Variables
As noted above, environment variables are not secrets. Even if you reference an .env file for Compose variable substitution, the resolved values end up in the container's environment and are easily exposed. If you must use environment variables for legacy apps, consider an entrypoint script that reads the secret file and sets the variable, then immediately unsets it after the app starts.
2. Leaking Secrets via Build Args
Build arguments (ARG) are part of the image's build history and can be inspected with docker history. Never pass secrets as build args:
# Dangerous: secret ends up in image layers
ARG DB_PASSWORD=supersecret
RUN echo "use $DB_PASSWORD"
Instead, use multi-stage builds or secret mounts (available with BuildKit) during the build phase, and rely on runtime secrets for final execution.
3. Hardcoding Secret Values in Compose Files
Never write the secret value directly in the Compose file:
secrets:
db_password:
# No! This is plain text visible to everyone with access to the Compose file.
file: ./secrets/db_password.txt
# content: "supersecret" # hypothetical but dangerous if supported
The file attribute should point to a path, not contain the secret. The secret file itself must be excluded from version control via .gitignore.
4. Not Cleaning Up Secrets After Use
In Swarm mode, secrets persist until explicitly removed. If you decommission a service, its secrets might still exist on nodes. Regularly prune unused secrets with docker secret rm or automated cleanup scripts.
5. Assuming Secrets Are Encrypted at Rest in Local Compose
Local Compose secrets (file-based) are simply bind mounts. They are not encrypted at rest. Anyone with host filesystem access can read them. For local development this is acceptable as long as you don't commit the files, but never treat them as fully secure in the same way as Swarm secrets.
6. Exposing Secrets to Unnecessary Services
Grant secrets only to the services that absolutely need them. Avoid global secret lists that every service inherits. This limits the blast radius if a service is compromised.
Best Practices
Use External Secret Management for Production
For production deployments on Docker Swarm, use the built-in encrypted secrets:
# Create a secret from standard input
echo "my-production-password" | docker secret create db_password -
# In your Compose file, declare it as external
version: '3.8'
services:
web:
image: myapp:prod
secrets:
- db_password
secrets:
db_password:
external: true
For non-Swarm orchestrators (Kubernetes, Nomad) or cloud platforms, integrate with dedicated secret stores like HashiCorp Vault, AWS Secrets Manager, or Azure Key Vault. Use init containers or entrypoint scripts to fetch secrets at startup and write them to /run/secrets/ so your application sees a consistent interface.
Restrict Secret Access to Specific Services
In your Compose file, only list secrets under the services that need them:
services:
frontend:
image: nginx
# No secrets needed
backend:
image: myapp
secrets:
- db_password
- api_key
This principle of least privilege limits accidental exposure.
Rotate Secrets Regularly
Implement secret rotation by updating the secret in the store and then redeploying services. With Swarm, you can update a secret and then trigger a rolling update of the service:
# Update secret value (creates a new version)
echo "new-password" | docker secret create db_password_v2 -
# Remove old secret later
docker secret rm db_password
Plan for downtime or graceful reload by having your application watch the secret file for changes, or simply restart.
Never Commit Secrets to Version Control
Add secret files to .gitignore and provide template files (e.g., db_password.txt.example) instead. Use a secrets manager or a team-wide tool like sops to encrypt secrets before they touch version control.
# .gitignore
secrets/
*.secret
.env
Use Docker Swarm for Production Secret Security
If you're already using Docker Compose for orchestration, consider migrating to Docker Swarm for built-in encrypted secrets, mutual TLS, and automatic secret distribution. Swarm secrets are encrypted in transit and at rest on the manager nodes, and only decrypted on the worker node that runs the service. This dramatically reduces the risk of secret interception compared to bind mounts.
Validate Secrets in Entrypoint Scripts
Always validate that required secrets exist at container startup, and fail fast with a clear error message:
#!/bin/sh
if [ ! -f /run/secrets/db_password ]; then
echo "FATAL: db_password secret missing" >&2
exit 1
fi
This prevents a container from running with a missing secret and producing obscure errors later.
Conclusion
Docker Compose secrets provide a clean, file-based abstraction that helps you keep sensitive data out of images, environment variables, and version control. By understanding the difference between local bind mounts and Swarm's encrypted secrets, avoiding common pitfalls like environment variable leakage and build-arg exposure, and following best practices such as least-privilege access, regular rotation, and external secret store integration, you can significantly improve the security posture of your containerized applications. Start using secrets today—even in local development—to build secure habits that translate seamlessly to production.