Back to BlogDocker Container · Docker compose · docker

Secrets and Configs in Docker Compose

2025-12-26

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:

  1. Deploy the application with the new secret name
  2. Verify the application works correctly
  3. Remove old secret files from the host
  4. 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:

  1. Create new config version: app_config_v2
  2. Update Compose file to reference new config
  3. 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.

We use cookies to improve your experience and analyse site traffic. See our Privacy Policy.