Docker Secrets vs .env Files: Secure Configuration Management
A comprehensive comparison of Docker secrets and traditional .env files for managing sensitive configuration. Learn why Docker secrets provide better security, how to migrate from .env files, and best practices for production deployments.
Managing sensitive configuration — database passwords, API keys, TLS certificates — is one of the most critical aspects of containerized applications. Most developers start with .env files, but Docker provides a built-in secrets mechanism that is fundamentally more secure.
This article explains why Docker secrets exist, how they work, and when you should use them over traditional .env files.
The Problem with .env Files
The .env file approach is simple: define key-value pairs and reference them in your docker-compose.yml:
# .env
POSTGRES_PASSWORD=super_secret_123
API_KEY=sk-abc123def456
REDIS_PASSWORD=redis_pass_789
# docker-compose.yml
services:
app:
image: myapp
env_file:
- .env
This works, but has serious security issues:
1. Environment variables are visible everywhere
Anyone who can run docker inspect on your container sees every secret in plain text:
docker inspect my_container --format '{{json .Config.Env}}'
# Output: ["POSTGRES_PASSWORD=super_secret_123", "API_KEY=sk-abc123def456", ...]
They also appear in /proc/<pid>/environ inside the container, in docker-compose config output, and in any child process that inherits the environment.
2. Logging and debugging often leak secrets
Crash reports, debugging tools, and monitoring agents frequently dump environment variables. A single env command or stack trace can expose everything.
3. .env files are easy to commit accidentally
Despite .gitignore rules, .env files get committed to version control regularly. Once in git history, the secret is exposed permanently — even if you delete the file later.
4. No access control
Every service in your Docker Compose stack that uses env_file gets all the secrets, even if it only needs one. There is no way to restrict which service sees which secret.
How Docker Secrets Work
Docker secrets provide an encrypted, access-controlled mechanism for distributing sensitive data to containers.
Key principles:
- Secrets are stored encrypted at rest (in Docker Swarm's Raft log)
- They are mounted as files inside the container at
/run/secrets/<secret_name> - They are only available to services that are explicitly granted access
- They never appear in
docker inspect, environment variables, or logs - They exist only in memory (tmpfs) inside the container — never written to disk
Docker Secrets in Docker Compose (v3.1+)
You don't need a full Swarm cluster. Docker Compose supports secrets with file-based sources:
# docker-compose.yml
version: "3.8"
services:
db:
image: postgres:15
secrets:
- db_password
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
app:
image: myapp
secrets:
- db_password
- api_key
# Read secrets in your code from /run/secrets/
redis:
image: redis:7
# redis does NOT have access to db_password or api_key
secrets:
db_password:
file: ./secrets/db_password.txt
api_key:
file: ./secrets/api_key.txt
Create the secret files:
mkdir -p secrets
echo "super_secret_123" > secrets/db_password.txt
echo "sk-abc123def456" > secrets/api_key.txt
chmod 600 secrets/*.txt
Reading Secrets in Your Application
Inside the container, secrets appear as plain files:
# Inside the container
cat /run/secrets/db_password
# Output: super_secret_123
Python example:
import os
def read_secret(name, default=None):
"""Read a Docker secret from /run/secrets/ or fall back to env var."""
secret_path = f"/run/secrets/{name}"
if os.path.exists(secret_path):
with open(secret_path, "r") as f:
return f.read().strip()
return os.environ.get(name.upper(), default)
# Usage
db_password = read_secret("db_password")
api_key = read_secret("api_key")
Node.js example:
const fs = require('fs');
const path = require('path');
function readSecret(name, fallback) {
const secretPath = path.join('/run/secrets', name);
try {
return fs.readFileSync(secretPath, 'utf8').trim();
} catch {
return process.env[name.toUpperCase()] || fallback;
}
}
const dbPassword = readSecret('db_password');
This pattern lets your code work both with Docker secrets (production) and environment variables (local development).
The _FILE Convention
Many official Docker images (PostgreSQL, MySQL, MariaDB, Redis) support the _FILE suffix convention. Instead of passing the secret as an environment variable, you pass the path to the secret file:
services:
db:
image: postgres:15
secrets:
- db_password
environment:
# Instead of: POSTGRES_PASSWORD=super_secret_123
# Use:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
The image's entrypoint script reads the file and sets the real environment variable internally — the secret never touches the container's environment.
Comparison: .env Files vs Docker Secrets
| Criteria | .env Files | Docker Secrets |
|---|---|---|
| Storage at rest | Plain text on disk | Encrypted (Swarm) or file-based (Compose) |
| Runtime exposure | Visible in docker inspect, /proc/environ, logs | Mounted as in-memory file at /run/secrets/ only |
| Access control | All services see all variables | Per-service: only granted services can read |
| Accidental git commit | High risk (.env in project root) | Lower risk (separate secrets/ dir, easier to gitignore) |
| docker inspect leaks | Yes - all env vars visible | No - secrets not in inspect output |
| Child process inheritance | Yes - all child processes inherit env | No - must explicitly read the file |
| Rotation | Requires container restart | Can update secret file, container reads on demand |
| Multi-line values | Problematic (newlines in env vars) | Easy (TLS certs, SSH keys as files) |
| Setup complexity | Simple - one file | Slightly more - separate files + compose config |
| Local development | Very easy | Works, but slightly more setup |
| Production suitability | Not recommended for sensitive data | Recommended |
A Practical Migration Example
Let's convert a typical .env-based setup to Docker secrets.
Before (using .env):
# .env
POSTGRES_PASSWORD=prod_db_pass_2024
REDIS_PASSWORD=prod_redis_pass
SECRET_KEY=flask_secret_key_abc123
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG
# docker-compose.yml
services:
flask:
build: .
env_file: .env
postgres:
image: postgres:15
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
redis:
image: redis:7
command: redis-server --requirepass ${REDIS_PASSWORD}
After (using Docker secrets):
# docker-compose.yml
version: "3.8"
services:
flask:
build: .
secrets:
- db_password
- redis_password
- flask_secret_key
- aws_secret_key
environment:
# Non-sensitive config stays as env vars
FLASK_ENV: production
POSTGRES_HOST: postgres
REDIS_HOST: redis
postgres:
image: postgres:15
secrets:
- db_password
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
redis:
image: redis:7
secrets:
- redis_password
command: >
sh -c "redis-server --requirepass $$(cat /run/secrets/redis_password)"
secrets:
db_password:
file: ./secrets/db_password.txt
redis_password:
file: ./secrets/redis_password.txt
flask_secret_key:
file: ./secrets/flask_secret_key.txt
aws_secret_key:
file: ./secrets/aws_secret_key.txt
Notice that:
- Non-sensitive config (
FLASK_ENV,POSTGRES_HOST) stays as regular environment variables - Sensitive data (passwords, keys) moves to secrets
- PostgreSQL uses the built-in
_FILEconvention - Redis reads the secret file in its command
- Flask reads secrets via the
read_secret()helper function
When to Use What
Use .env files for:
- Local development and quick prototyping
- Non-sensitive configuration (feature flags, port numbers, hostnames)
- Variables that are intentionally public (e.g.,
NODE_ENV=production)
Use Docker secrets for:
- Database passwords
- API keys and tokens
- TLS/SSL certificates and private keys
- SSH keys
- Any credential that would cause damage if leaked
The hybrid approach works best in practice:
services:
app:
env_file:
- .env # Non-sensitive: ports, hosts, feature flags
secrets:
- db_password # Sensitive: passwords, keys, certs
- api_key
Best Practices
1. Never store secrets in your Docker image
# BAD - secret baked into image layer
ENV API_KEY=sk-abc123
COPY .env /app/.env
# GOOD - secret injected at runtime via mount
# No secrets in Dockerfile at all
2. Use .dockerignore to prevent accidental inclusion
# .dockerignore
.env
secrets/
*.pem
*.key
3. Set proper file permissions on secret files
chmod 600 secrets/*.txt
chown root:root secrets/*.txt
4. Add secrets directory to .gitignore
# .gitignore
.env
secrets/
5. Use a secrets manager for larger deployments
For production at scale, consider:
- HashiCorp Vault — full-featured secrets management
- AWS Secrets Manager / Azure Key Vault / GCP Secret Manager — cloud-native
- SOPS (Secrets OPerationS) — encrypts secrets in git
These integrate with Docker secrets or replace them entirely.
Summary
.env files are convenient but fundamentally insecure for sensitive data. Docker secrets provide a purpose-built mechanism that keeps credentials out of environment variables, docker inspect output, logs, and child processes.
The migration is straightforward: move sensitive values to secret files, update your docker-compose.yml to mount them, and modify your application to read from /run/secrets/. Keep non-sensitive config in environment variables.
For any production deployment handling real user data, Docker secrets (or an external secrets manager) should be the default choice.