Sensitive data like passwords, API keys, certificates, and configuration files require special handling in containerized applications. Docker Compose provides two distinct mechanisms for managing this data: secrets for sensitive credentials that must remain encrypted and protected, and configs for non-sensitive configuration files that need to be managed separately from images. Understanding these mechanisms is essential for building secure, maintainable containerized applications.
Understanding Secrets vs Configs
Secrets and configs serve different purposes:
Secrets are for sensitive data:
- Database passwords
- API keys and tokens
- TLS certificates and private keys
- OAuth credentials
- Encryption keys
Configs are for non-sensitive configuration:
- Application configuration files
- Server configuration templates
- Static content
- Public certificates
The key distinction is sensitivity. Secrets receive special security handling, while configs are optimized for managing configuration files that don't require encryption.
Basic Secrets Definition
Define secrets in the top-level secrets section of your Compose file:
version: '3.8'
services:
database:
image: postgres:14
secrets:
- db_password
environment:
- POSTGRES_PASSWORD_FILE=/run/secrets/db_password
secrets:
db_password:
file: ./secrets/db_password.txt
The secret is loaded from ./secrets/db_password.txt on the host and made available inside the container at /run/secrets/db_password. The application reads the secret from this file rather than from an environment variable.
Creating Secret Files
Store secrets in files with restricted permissions:
mkdir -p secrets echo "my-secure-password" > secrets/db_password.txt chmod 600 secrets/db_password.txt
The 600 permission ensures only the file owner can read or write the file, preventing unauthorized access to sensitive credentials.
Multiple Secrets Per Service
Services often need multiple secrets:
version: '3.8'
services:
api:
image: myapp/api:latest
secrets:
- db_password
- api_key
- jwt_secret
environment:
- DB_PASSWORD_FILE=/run/secrets/db_password
- API_KEY_FILE=/run/secrets/api_key
- JWT_SECRET_FILE=/run/secrets/jwt_secret
secrets:
db_password:
file: ./secrets/db_password.txt
api_key:
file: ./secrets/api_key.txt
jwt_secret:
file: ./secrets/jwt_secret.txt
Each secret appears as a separate file in /run/secrets/ inside the container.
Secret Mount Customization
Customize where secrets appear inside containers:
version: '3.8'
services:
web:
image: nginx:alpine
secrets:
- source: site_certificate
target: /etc/nginx/ssl/cert.pem
uid: '0'
gid: '0'
mode: 0400
secrets:
site_certificate:
file: ./secrets/certificate.pem
This mounts the secret at a custom path (/etc/nginx/ssl/cert.pem) with specific ownership (root:root) and permissions (read-only for owner).
External Secrets
Reference secrets created outside your Compose file:
version: '3.8'
services:
api:
image: myapp/api:latest
secrets:
- db_password
secrets:
db_password:
external: true
External secrets must be created manually before running docker-compose up:
echo "my-password" | docker secret create db_password -
This pattern is useful when secrets are managed by external systems or shared across multiple Compose projects.
External Secrets with Name Mapping
Map internal secret names to different external names:
version: '3.8'
services:
api:
image: myapp/api:latest
secrets:
- database_password
secrets:
database_password:
external: true
name: production_db_password
The service references database_password internally, but Compose looks for an external secret named production_db_password.
Basic Configs Definition
Define configs similarly to secrets:
version: '3.8'
services:
web:
image: nginx:alpine
configs:
- nginx_config
configs:
nginx_config:
file: ./configs/nginx.conf
The config file is mounted at /nginx_config inside the container by default.
Custom Config Mount Points
Specify exactly where configs should appear:
version: '3.8'
services:
web:
image: nginx:alpine
configs:
- source: nginx_config
target: /etc/nginx/nginx.conf
mode: 0444
configs:
nginx_config:
file: ./configs/nginx.conf
This places the config at the standard nginx configuration path with read-only permissions for all users.
Multiple Configs Per Service
Services can use multiple configuration files:
version: '3.8'
services:
web:
image: nginx:alpine
configs:
- source: nginx_main
target: /etc/nginx/nginx.conf
- source: default_site
target: /etc/nginx/conf.d/default.conf
- source: ssl_params
target: /etc/nginx/conf.d/ssl-params.conf
configs:
nginx_main:
file: ./configs/nginx.conf
default_site:
file: ./configs/default.conf
ssl_params:
file: ./configs/ssl-params.conf
Each config is mounted at its specified location, building a complete configuration structure.
External Configs
Reference configs managed outside your Compose file:
version: '3.8'
services:
web:
image: nginx:alpine
configs:
- source: nginx_config
target: /etc/nginx/nginx.conf
configs:
nginx_config:
external: true
Create external configs manually:
docker config create nginx_config ./nginx.conf
External configs are immutable once created—to update them, you must create a new config with a different name.
Config and Secret Labels
Add metadata to configs and secrets:
version: '3.8'
secrets:
db_password:
file: ./secrets/db_password.txt
labels:
com.example.description: "Database root password"
com.example.created: "2024-01-15"
com.example.rotation-required: "true"
configs:
nginx_config:
file: ./configs/nginx.conf
labels:
com.example.description: "Main nginx configuration"
com.example.version: "1.2.0"
Labels help document and organize secrets and configs, especially in environments with many configuration items.
Combining Secrets and Configs
Services typically use both secrets and configs:
version: '3.8'
services:
api:
image: myapp/api:latest
secrets:
- db_password
- api_key
configs:
- source: app_config
target: /app/config.yaml
environment:
- DB_PASSWORD_FILE=/run/secrets/db_password
- API_KEY_FILE=/run/secrets/api_key
- CONFIG_FILE=/app/config.yaml
secrets:
db_password:
file: ./secrets/db_password.txt
api_key:
file: ./secrets/api_key.txt
configs:
app_config:
file: ./configs/app-config.yaml
This separates sensitive credentials (secrets) from application configuration (configs).
Reading Secrets in Applications
Applications must be designed to read secrets from files. Here's a pattern in various languages:
Node.js:
const fs = require('fs');
const dbPassword = fs.readFileSync('/run/secrets/db_password', 'utf8').trim();
Python:
def read_secret(secret_name):
with open(f'/run/secrets/{secret_name}', 'r') as f:
return f.read().strip()
db_password = read_secret('db_password')
Go:
func readSecret(name string) (string, error) {
data, err := os.ReadFile("/run/secrets/" + name)
if err != nil {
return "", err
}
return strings.TrimSpace(string(data)), nil
}
Applications should handle missing secrets gracefully and provide clear error messages when secrets cannot be read.
Secret File Permissions
Control secret file permissions inside containers:
version: '3.8'
services:
api:
image: myapp/api:latest
user: "1000:1000"
secrets:
- source: private_key
target: /app/keys/private.key
uid: '1000'
gid: '1000'
mode: 0400
secrets:
private_key:
file: ./secrets/private.key
The secret is owned by UID 1000, GID 1000 (matching the service's user) with read-only permissions for the owner only.
Template Configs with Variable Substitution
Configs can include environment variable references that Docker Compose resolves:
version: '3.8'
services:
web:
image: nginx:alpine
environment:
- SERVER_NAME=example.com
configs:
- source: nginx_template
target: /etc/nginx/conf.d/default.conf
configs:
nginx_template:
file: ./configs/nginx-template.conf
nginx-template.conf:
server {
listen 80;
server_name ${SERVER_NAME};
location / {
root /usr/share/nginx/html;
}
}
Docker Compose substitutes ${SERVER_NAME} with the environment variable value when creating the config.
Secrets for Database Initialization
Use secrets to initialize databases securely:
version: '3.8'
services:
database:
image: postgres:14
secrets:
- postgres_password
- postgres_user
environment:
- POSTGRES_PASSWORD_FILE=/run/secrets/postgres_password
- POSTGRES_USER_FILE=/run/secrets/postgres_user
- POSTGRES_DB=myapp
secrets:
postgres_password:
file: ./secrets/postgres_password.txt
postgres_user:
file: ./secrets/postgres_user.txt
PostgreSQL's official image supports reading credentials from files, avoiding exposure through environment variables.
Certificate Management with Secrets
Manage TLS certificates securely:
version: '3.8'
services:
web:
image: nginx:alpine
secrets:
- ssl_certificate
- ssl_certificate_key
configs:
- source: ssl_nginx_config
target: /etc/nginx/conf.d/ssl.conf
ports:
- "443:443"
secrets:
ssl_certificate:
file: ./secrets/certificate.crt
ssl_certificate_key:
file: ./secrets/certificate.key
configs:
ssl_nginx_config:
file: ./configs/ssl.conf
ssl.conf:
server {
listen 443 ssl;
ssl_certificate /run/secrets/ssl_certificate;
ssl_certificate_key /run/secrets/ssl_certificate_key;
location / {
root /usr/share/nginx/html;
}
}
Certificates and keys are managed as secrets, while the nginx configuration referencing them is a config.
API Key Management
Distribute API keys to multiple services:
version: '3.8'
services:
api-gateway:
image: gateway:latest
secrets:
- external_api_key
- internal_api_key
environment:
- EXTERNAL_API_KEY_FILE=/run/secrets/external_api_key
- INTERNAL_API_KEY_FILE=/run/secrets/internal_api_key
backend-service:
image: backend:latest
secrets:
- internal_api_key
environment:
- API_KEY_FILE=/run/secrets/internal_api_key
secrets:
external_api_key:
file: ./secrets/external_api.txt
internal_api_key:
file: ./secrets/internal_api.txt
The gateway needs both external and internal API keys, while the backend service only needs the internal key.
Configuration Version Control
While secrets should never be committed to version control, configs can be:
version: '3.8'
services:
web:
image: nginx:alpine
secrets:
- ssl_key # Not in git
configs:
- nginx_config # In git
secrets:
ssl_key:
file: ./secrets/ssl.key # In .gitignore
configs:
nginx_config:
file: ./configs/nginx.conf # Committed to git
.gitignore:
secrets/ *.key *.pem
This separates version-controlled configs from secrets that must remain outside version control.
Config Templates for Different Services
Share base configs across services with service-specific customization:
version: '3.8'
services:
api-v1:
image: myapp/api:v1
configs:
- source: base_config
target: /app/base.yaml
- source: v1_config
target: /app/service.yaml
api-v2:
image: myapp/api:v2
configs:
- source: base_config
target: /app/base.yaml
- source: v2_config
target: /app/service.yaml
configs:
base_config:
file: ./configs/base.yaml
v1_config:
file: ./configs/v1-specific.yaml
v2_config:
file: ./configs/v2-specific.yaml
Both services share the base configuration while using version-specific configs for customization.
Environment-Specific Secret Files
Structure secret files for different deployment contexts:
version: '3.8'
services:
api:
image: myapp/api:latest
secrets:
- db_password
secrets:
db_password:
file: ./secrets/${ENVIRONMENT:-dev}/db_password.txt
Directory structure:
secrets/
├── dev/
│ └── db_password.txt
├── staging/
│ └── db_password.txt
└── production/
└── db_password.txt
Set ENVIRONMENT to load the appropriate secret file:
ENVIRONMENT=production docker-compose up
Secret Rotation Patterns
Implement secret rotation by creating new secrets and updating references:
version: '3.8'
services:
api:
image: myapp/api:latest
secrets:
- db_password_v2 # Updated from db_password_v1
secrets:
db_password_v2:
file: ./secrets/db_password_v2.txt
After deployment with the new secret:
- Deploy the application with the new secret name
- Verify the application works correctly
- Remove old secret files from the host
- Update documentation
Handling Missing Secrets
Configure services to handle missing secrets gracefully:
version: '3.8'
services:
api:
image: myapp/api:latest
secrets:
- db_password
environment:
- REQUIRE_SECRETS=true
- DB_PASSWORD_FILE=/run/secrets/db_password
secrets:
db_password:
file: ./secrets/db_password.txt
Application startup script:
#!/bin/sh
if [ "$REQUIRE_SECRETS" = "true" ]; then
if [ ! -f "$DB_PASSWORD_FILE" ]; then
echo "ERROR: Required secret file not found: $DB_PASSWORD_FILE"
exit 1
fi
fi
exec "$@"
Config-Driven Service Behavior
Use configs to control service features:
version: '3.8'
services:
api:
image: myapp/api:latest
configs:
- source: feature_flags
target: /app/features.json
- source: logging_config
target: /app/logging.yaml
configs:
feature_flags:
file: ./configs/features.json
logging_config:
file: ./configs/logging.yaml
features.json:
{
"enable_analytics": true,
"enable_beta_features": false,
"cache_ttl": 3600
}
Applications read these configs at startup or runtime to adjust behavior without code changes.
Secrets in Multi-Service Applications
Complex applications distribute secrets appropriately:
version: '3.8'
services:
frontend:
image: myapp/frontend:latest
secrets:
- api_token
configs:
- frontend_config
api:
image: myapp/api:latest
secrets:
- db_password
- redis_password
- jwt_secret
configs:
- api_config
database:
image: postgres:14
secrets:
- db_password
cache:
image: redis:alpine
secrets:
- redis_password
secrets:
api_token:
file: ./secrets/api_token.txt
db_password:
file: ./secrets/db_password.txt
redis_password:
file: ./secrets/redis_password.txt
jwt_secret:
file: ./secrets/jwt_secret.txt
configs:
frontend_config:
file: ./configs/frontend.json
api_config:
file: ./configs/api.yaml
Each service receives only the secrets it needs, following the principle of least privilege.
Secret Naming Conventions
Establish clear naming conventions for secrets:
version: '3.8'
secrets:
# Database credentials
db_postgres_password:
file: ./secrets/db-postgres-password.txt
db_postgres_user:
file: ./secrets/db-postgres-user.txt
# API keys
api_external_stripe_key:
file: ./secrets/api-stripe-key.txt
api_external_sendgrid_key:
file: ./secrets/api-sendgrid-key.txt
# Certificates
tls_web_certificate:
file: ./secrets/tls-web-cert.pem
tls_web_private_key:
file: ./secrets/tls-web-key.pem
Consistent naming improves maintainability and reduces confusion in large applications.
Config Layering Strategy
Layer configs from general to specific:
version: '3.8'
services:
api:
image: myapp/api:latest
configs:
- source: base_config
target: /app/config/base.yaml
- source: database_config
target: /app/config/database.yaml
- source: cache_config
target: /app/config/cache.yaml
- source: logging_config
target: /app/config/logging.yaml
configs:
base_config:
file: ./configs/base.yaml
database_config:
file: ./configs/database.yaml
cache_config:
file: ./configs/cache.yaml
logging_config:
file: ./configs/logging.yaml
Applications merge these configs in order, allowing general settings in base.yaml and specific overrides in specialized configs.
Secrets for Service Authentication
Enable secure service-to-service authentication:
version: '3.8'
services:
api-gateway:
image: gateway:latest
secrets:
- service_auth_token
environment:
- SERVICE_TOKEN_FILE=/run/secrets/service_auth_token
backend:
image: backend:latest
secrets:
- service_auth_token
environment:
- EXPECTED_TOKEN_FILE=/run/secrets/service_auth_token
secrets:
service_auth_token:
file: ./secrets/service_token.txt
Both services share the same authentication token, enabling the backend to verify requests from the gateway.
Dynamic Config Updates
While configs are immutable after creation, you can implement dynamic reloading:
version: '3.8'
services:
api:
image: myapp/api:latest
configs:
- source: app_config_v1
target: /app/config.yaml
command: ["./api", "--watch-config", "/app/config.yaml"]
configs:
app_config_v1:
file: ./configs/app-v1.yaml
When you need to update configuration:
- Create new config version: app_config_v2
- Update Compose file to reference new config
- Redeploy services
Applications that watch config files can reload without restart if they support hot reloading.
Secret Access Auditing
Implement audit logging for secret access:
version: '3.8'
services:
api:
image: myapp/api:latest
secrets:
- db_password
configs:
- source: audit_config
target: /app/audit.yaml
environment:
- ENABLE_SECRET_AUDIT=true
- DB_PASSWORD_FILE=/run/secrets/db_password
secrets:
db_password:
file: ./secrets/db_password.txt
configs:
audit_config:
file: ./configs/audit.yaml
Applications log when secrets are accessed, who accessed them, and when, providing an audit trail for security compliance.
Temporary Secrets for Development
Use different secret management strategies for development:
version: '3.8'
services:
api:
image: myapp/api:latest
secrets:
- db_password
environment:
- ALLOW_DEFAULT_SECRETS=${ALLOW_DEFAULT_SECRETS:-true}
secrets:
db_password:
file: ./secrets/${ENVIRONMENT:-dev}/db_password.txt
Development environments can use default/simple secrets, while production environments require proper secret management with ALLOW_DEFAULT_SECRETS=false.
Best Practices for Secrets Management
Never commit secrets to version control: Use .gitignore to exclude secret directories and files from git repositories.
Use strong, unique secrets: Generate random secrets for each credential using tools like openssl rand -base64 32.
Rotate secrets regularly: Implement processes for regular secret rotation, especially for production systems.
Limit secret scope: Grant each service access only to the secrets it needs.
Use file-based secrets over environment variables: Environment variables can leak through logs, process listings, and error messages.
Protect secret files on the host: Set restrictive file permissions (600 or 400) on secret files.
Document secret purposes: Use labels and comments to document what each secret is used for.
Implement secret validation: Verify secrets meet security requirements (length, complexity, format) before deployment.
Monitor secret access: Log and monitor when secrets are read by applications for security auditing.
Plan for secret compromise: Have procedures ready for secret rotation when security incidents occur.
Best Practices for Configs Management
Version control configs: Unlike secrets, configs should be committed to version control for tracking and collaboration.
Use descriptive config names: Name configs clearly to indicate their purpose and the services they configure.
Validate config syntax: Test configs before deployment to catch syntax errors early.
Document config options: Include comments in config files explaining available options and their effects.
Use configs for environment-independent settings: Place environment-specific values in environment variables or secrets, not configs.
Organize configs by service: Group related configs together for easier maintenance.
Test config changes: Verify config changes in non-production environments before production deployment.
Maintain config versioning: When updating configs, consider versioning them to enable rollback.
Docker Compose secrets and configs provide powerful mechanisms for managing sensitive credentials and configuration data. By properly implementing these patterns, you protect sensitive information, maintain clean separation between code and configuration, and build applications that are secure, maintainable, and portable across different environments. The distinction between secrets and configs enables appropriate security controls while maintaining operational flexibility.