← Back to DevBytes

Docker Secrets Management: Production Guide

Introduction to Docker Secrets

Docker Secrets provide a secure mechanism for managing sensitive data such as API keys, database passwords, TLS certificates, and other confidential configuration values that your containerized applications need at runtime. Instead of baking secrets into images or passing them through insecure environment variables, Docker Secrets delivers them securely to containers at runtime, encrypted during transit and at rest, and mounted as in-memory files that never touch disk in an unencrypted form.

This mechanism is a native feature of Docker Swarm mode, but can also be emulated in single-node development environments using Docker Compose. Understanding how to leverage Docker Secrets properly is essential for any team deploying containerized workloads in production.

Why Docker Secrets Matter in Production

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Before Docker Secrets became widely adopted, developers commonly resorted to risky practices for injecting configuration into containers:

Docker Secrets addresses all these concerns by providing a cryptographically secure distribution channel that ensures secrets are only available to the specific services that explicitly request them, and only while those services are running. This aligns with the principle of least privilege and dramatically reduces the blast radius of a credential leak.

How Docker Secrets Work Under the Hood

When you create a secret in Docker Swarm, the following process occurs:

This architecture ensures end-to-end encryption from the point of creation to the point of consumption, with no persistent unencrypted storage anywhere in the pipeline.

Prerequisites for Using Docker Secrets

Native Docker Secrets are a feature of Docker Swarm mode. To use them, you must first initialize a Swarm:

# Initialize a single-node swarm (suitable for development or small production deployments)
docker swarm init

# Or, if you already have a multi-node cluster, ensure you are operating on a manager node
docker node ls

Once Swarm mode is active, you can create, list, and manage secrets using the docker secret command family.

Creating and Managing Docker Secrets

Creating a Secret from a String

The most direct way to create a secret is by piping a value via stdin:

# Create a secret named 'db_password' with a plaintext value
echo "SuperSecretP@ssw0rd!" | docker secret create db_password -

# The trailing dash tells Docker to read from stdin

You can also use printf to avoid newline artifacts that echo may introduce:

printf "SuperSecretP@ssw0rd!" | docker secret create db_password -

Creating a Secret from a File

For secrets that are already stored in files (such as TLS certificates or generated API keys), you can reference the file directly:

# Create a secret from an existing file
docker secret create tls_certificate /path/to/fullchain.pem

# Create a secret with a descriptive name
docker secret create nginx_tls_key /etc/letsencrypt/live/example.com/privkey.pem

Listing and Inspecting Secrets

# List all secrets in the swarm
docker secret ls

# Inspect metadata for a specific secret (note: the value is NOT displayed)
docker secret inspect db_password

Output from docker secret inspect shows the secret's ID, name, creation timestamp, and labels — but never the actual secret value. This is by design to prevent accidental exposure.

Removing Secrets

# Remove a secret (only works if no running service is currently using it)
docker secret rm db_password

# Force removal even if a service references it (service continues running but loses access)
docker secret rm --force db_password

Be cautious with --force — services that lose access to a secret mid-run may crash or behave unpredictably. Always update services to remove secret dependencies before deleting the secret.

Using Secrets with Docker Services

Mounting Secrets into a Service

Secrets are attached to services at creation or update time using the --secret flag:

# Create a service that uses a secret
docker service create \
  --name my_app \
  --secret db_password \
  --replicas 3 \
  my_app_image:latest

By default, the secret is mounted at /run/secrets/db_password inside the container as a plaintext file. Your application code reads the secret by opening this file:

# Example Python code to read a Docker Secret
def get_secret(secret_name):
    try:
        with open(f'/run/secrets/{secret_name}', 'r') as secret_file:
            return secret_file.read().strip()
    except FileNotFoundError:
        return None

# Usage
database_password = get_secret('db_password')

Customizing Secret Mount Location and Permissions

You can override the default mount point and set file permissions using extended syntax:

docker service create \
  --name my_app \
  --secret source=db_password,target=/etc/app/secrets/db_password,mode=0400 \
  my_app_image:latest

Here, source is the secret name in Docker, target is the path inside the container, and mode sets the file permissions (0400 restricts read access to the file owner only).

Updating Secrets on a Running Service

Docker does not allow editing a secret's value directly. To rotate a secret, you must create a new secret and update the service to use it:

# Step 1: Create the new secret with a different name (e.g., versioned)
echo "NewRotatedP@ssw0rd!" | docker secret create db_password_v2 -

# Step 2: Update the service to use the new secret and remove the old one
docker service update \
  --secret-rm db_password \
  --secret-add source=db_password_v2,target=/run/secrets/db_password \
  my_app

# Step 3: Once all tasks converge on the new secret, remove the old one
docker secret rm db_password

This approach supports zero-downtime secret rotation. The service tasks restart one by one (following the update policy), each picking up the new secret version.

Using Docker Secrets with Docker Compose

Docker Compose supports secrets in a Swarm-compatible way when deploying stacks. The Compose file defines secrets at the top level and references them in service definitions:

# docker-compose.yml (version 3.1 or higher required for secrets)
version: '3.8'

secrets:
  db_password:
    file: ./secrets/db_password.txt
  api_key:
    external: true  # This secret must already exist in the swarm

services:
  web:
    image: my_web_app:latest
    secrets:
      - db_password
      - api_key
    environment:
      - SECRET_PATH=/run/secrets
  
  db:
    image: postgres:15
    secrets:
      - source: db_password
        target: /etc/postgres/password
        mode: 0400
    environment:
      - POSTGRES_PASSWORD_FILE=/etc/postgres/password

When deploying via docker stack deploy, Docker reads the secret files from the host and creates them in the swarm automatically. For local development without Swarm, Docker Compose can emulate secrets by bind-mounting the specified files into /run/secrets/, though this does not provide the same encryption guarantees as true Swarm secrets.

Deploying the Stack

# Deploy the stack to the swarm
docker stack deploy -c docker-compose.yml my_stack

# Verify secrets are attached
docker service inspect my_stack_web --format '{{json .Spec.TaskTemplate.ContainerSpec.Secrets}}' | jq

Common Patterns for Consuming Secrets in Application Code

Node.js Example

const fs = require('fs');
const path = require('path');

function readSecret(secretName) {
  const secretPath = path.join('/run/secrets', secretName);
  if (fs.existsSync(secretPath)) {
    return fs.readFileSync(secretPath, 'utf8').trim();
  }
  // Fallback to environment variable for local development
  return process.env[secretName.toUpperCase()] || null;
}

const dbPassword = readSecret('db_password');

Python Example with Caching

import os
from functools import lru_cache

@lru_cache(maxsize=32)
def get_secret(name: str) -> str:
    """Read a Docker secret, caching the result."""
    path = os.path.join('/run/secrets', name)
    try:
        with open(path, 'r') as f:
            return f.read().rstrip('\n')
    except FileNotFoundError:
        raise ValueError(f"Secret '{name}' not found at {path}")

# Usage
db_password = get_secret('db_password')

Go Example

package main

import (
    "fmt"
    "os"
    "path/filepath"
    "strings"
)

func ReadSecret(name string) (string, error) {
    data, err := os.ReadFile(filepath.Join("/run/secrets", name))
    if err != nil {
        return "", fmt.Errorf("failed to read secret %s: %w", name, err)
    }
    return strings.TrimSpace(string(data)), nil
}

func main() {
    password, err := ReadSecret("db_password")
    if err != nil {
        panic(err)
    }
    fmt.Println("Successfully loaded secret")
    _ = password // Use in your database connection
}

Docker Secrets in CI/CD Pipelines

Integrating Docker Secrets into your CI/CD pipeline requires careful orchestration. A typical workflow looks like this:

  1. Secrets are stored in a vault — HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, or your CI/CD platform's secret store.
  2. The deployment pipeline retrieves secrets at deploy time — Never bake secrets into build artifacts.
  3. The pipeline creates Docker secrets programmatically — Using the Docker CLI or API from within the deployment script.
  4. The stack is deployed or updated — Services reference the newly created secrets.

Example deployment script snippet:

#!/bin/bash
# deploy.sh — Retrieves secrets from HashiCorp Vault and creates Docker secrets

set -euo pipefail

# Authenticate to Vault and retrieve secrets
VAULT_TOKEN=$(vault write -field=token auth/approle/login role_id="$ROLE_ID" secret_id="$SECRET_ID")
DB_PASSWORD=$(vault read -field=password secret/data/production/database)

# Create Docker secrets (idempotent — remove if exists first)
docker secret rm db_password 2>/dev/null || true
echo "$DB_PASSWORD" | docker secret create db_password -

# Deploy the stack
docker stack deploy -c docker-compose.prod.yml my_stack

Best Practices for Docker Secrets Management

1. Never Log Secret Values

Ensure your application code never logs the contents of files read from /run/secrets. Implement logging filters or sanitizers that redact secret paths:

# Python logging filter example
import logging
import re

class SecretRedactionFilter(logging.Filter):
    def filter(self, record):
        # Redact any line containing /run/secrets/
        record.msg = re.sub(r'/run/secrets/\S+', '[REDACTED]', str(record.msg))
        return True

logger = logging.getLogger()
logger.addFilter(SecretRedactionFilter())

2. Use Short-Lived Secrets and Rotate Frequently

Implement automated rotation. Combine Docker Secrets with a secrets-as-a-service backend that generates ephemeral credentials. For example, use Vault's dynamic database credentials that are automatically revoked after a TTL expires.

3. Apply the Principle of Least Privilege

Only attach secrets to services that absolutely need them. A frontend web server likely doesn't need your database credentials — keep those scoped to the backend API service only.

4. Version Your Secrets

Adopt a naming convention that includes versioning, such as db_password_v3. This makes rollbacks straightforward and provides an audit trail of credential changes.

5. Set Restrictive File Permissions

Always use mode=0400 or mode=0600 to ensure only the container's root user (or a specific user via UID/GID mapping) can read the secret file. Avoid mode 0444 which allows any user in the container to read it.

6. Avoid Environment Variables for Production Secrets

Even though Docker Secrets can be mapped to environment variables indirectly, prefer reading from the secret file directly. Environment variables are inherited by child processes and are often exposed in debugging output.

7. Encrypt Secrets at Rest in the Swarm Raft Log

Enable Swarm encryption to protect the Raft log where secrets are stored:

# Initialize swarm with encrypted Raft log
docker swarm init --autolock

# Or enable autolock on an existing swarm
docker swarm update --autolock=true

# Store the unlock key in a secure vault — you'll need it after manager restarts

8. Monitor Secret Access Patterns

Use Docker's event stream or audit logs to track which services mount which secrets. Unusual secret access patterns can indicate misconfiguration or malicious activity.

9. Validate Secrets Before Deployment

Implement pre-deployment checks that verify secrets exist and are correctly formatted:

#!/bin/bash
# Pre-deployment validation
REQUIRED_SECRETS=("db_password" "api_key" "tls_cert")
for secret in "${REQUIRED_SECRETS[@]}"; do
  if ! docker secret inspect "$secret" &>/dev/null; then
    echo "ERROR: Required secret '$secret' is missing"
    exit 1
  fi
done
echo "All required secrets present"

10. Plan for Secret Recovery

Docker Secrets are immutable — once created, you cannot read their values back through the Docker API. Always store the original secret value in a secure external vault (HashiCorp Vault, AWS Secrets Manager, etc.) that supports retrieval, rotation, and audit logging. Docker Secrets should be the distribution mechanism, not the source of truth.

Security Considerations and Limitations

While Docker Secrets provides robust protection, there are important limitations to understand:

Integrating Docker Secrets with Orchestration Platforms

Kubernetes Comparison

If you're evaluating Docker Swarm against Kubernetes, note that Kubernetes offers a similar concept called Secrets (with a capital S). Kubernetes Secrets are base64-encoded (not encrypted by default) and stored in etcd. To achieve encryption parity with Docker Secrets, you must explicitly enable etcd encryption in Kubernetes. Docker Secrets encrypts during transit and at rest in the Raft log by default (with autolock). Both platforms support mounting secrets as files or environment variables.

Nomad and Other Schedulers

HashiCorp Nomad integrates natively with HashiCorp Vault for secret distribution, providing a more flexible alternative to Docker Secrets for non-Swarm deployments. If your infrastructure uses Nomad, leverage the Vault integration rather than attempting to retrofit Docker Secrets.

Real-World Deployment Example

Let's walk through a complete production deployment for a hypothetical three-tier application with a web frontend, an API backend, and a PostgreSQL database — all using Docker Secrets:

# File: docker-compose.prod.yml
version: '3.8'

secrets:
  postgres_password:
    external: true
  api_jwt_signing_key:
    external: true
  web_tls_cert:
    external: true
  web_tls_key:
    external: true

services:
  postgres:
    image: postgres:15-alpine
    secrets:
      - source: postgres_password
        target: /run/secrets/postgres_password
        mode: 0400
    environment:
      - POSTGRES_PASSWORD_FILE=/run/secrets/postgres_password
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - backend
    deploy:
      placement:
        constraints:
          - node.role == worker

  api:
    image: my_api:latest
    secrets:
      - source: postgres_password
        target: /run/secrets/db_password
        mode: 0400
      - source: api_jwt_signing_key
        target: /run/secrets/jwt_key
        mode: 0400
    environment:
      - DB_PASSWORD_FILE=/run/secrets/db_password
      - JWT_KEY_FILE=/run/secrets/jwt_key
    networks:
      - backend
      - frontend
    deploy:
      replicas: 3
      update_config:
        parallelism: 1
        delay: 10s
        order: start-first

  web:
    image: my_web:latest
    secrets:
      - source: web_tls_cert
        target: /etc/nginx/certs/fullchain.pem
        mode: 0444
      - source: web_tls_key
        target: /etc/nginx/certs/privkey.pem
        mode: 0400
    ports:
      - "443:443"
    networks:
      - frontend
    deploy:
      replicas: 2

networks:
  frontend:
  backend:

volumes:
  postgres_data:

Deployment commands:

# Step 1: Create secrets in the swarm
echo "secure_db_password_123!" | docker secret create postgres_password -
echo "jwt_signing_key_base64_encoded" | docker secret create api_jwt_signing_key -
docker secret create web_tls_cert /etc/letsencrypt/live/example.com/fullchain.pem
docker secret create web_tls_key /etc/letsencrypt/live/example.com/privkey.pem

# Step 2: Deploy the stack
docker stack deploy -c docker-compose.prod.yml production

# Step 3: Verify all services are running and secrets are mounted
docker service ls --filter "label=com.docker.stack.namespace=production"
docker service inspect production_api --format '{{json .Spec.TaskTemplate.ContainerSpec.Secrets}}' | jq

Debugging Secret-Related Issues

Common problems and their solutions:

Conclusion

Docker Secrets provides a robust, cryptographically sound mechanism for distributing sensitive configuration to containerized applications in production. By leveraging Swarm's built-in secret management, you eliminate the risks associated with hardcoded credentials, insecure environment variables, and unprotected configuration files. The key to success lies in treating Docker Secrets as a distribution layer within a broader secrets management strategy — always maintain an external source of truth like HashiCorp Vault or AWS Secrets Manager, implement regular rotation, follow the principle of least privilege, and never log or expose secret values. With these practices in place, Docker Secrets becomes an indispensable tool in your container security arsenal, enabling you to deploy with confidence even in the most security-sensitive environments.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles