Docker Swarm Secrets and Configs: Managing Sensitive Data in Production
Every production application needs credentials: database passwords, API keys, TLS certificates, OAuth tokens. The question is how those credentials reach your containers. Environment variables are the most common approach, and they are also the most dangerous. They leak into logs, appear in docker inspect output, and persist in image layers if set during build time.
Docker Swarm provides two native primitives for handling this problem: secrets and configs. Secrets are encrypted at rest in the Raft log, transmitted over TLS, and mounted as in-memory files inside containers. Configs follow the same distribution mechanism but are designed for non-sensitive configuration data. This guide covers both, along with rotation strategies, versioning patterns, and integration with external secret management tools like HashiCorp Vault.
Secrets vs. Environment Variables vs. Configs
| Feature | Environment Variables | Swarm Secrets | Swarm Configs |
|---|---|---|---|
| Encrypted at rest | No | Yes (Raft log AES-256) | No (stored in Raft, not encrypted) |
| Encrypted in transit | No (depends on how set) | Yes (mutual TLS) | Yes (mutual TLS) |
Visible in docker inspect |
Yes | No (only metadata) | Yes (content visible) |
| Mount location | Process environment | /run/secrets/<name> |
Configurable path |
| Max size | OS-dependent (~128KB) | 500 KB | 500 KB |
| Updateable without redeploy | No | Yes (via rotation) | Yes (via rotation) |
| Filesystem storage | N/A | tmpfs (RAM only) | tmpfs (RAM only) |
Rule of thumb: Use secrets for anything that would cause a security incident if exposed: passwords, private keys, API tokens. Use configs for non-sensitive configuration that you want to manage centrally: application config files, nginx.conf, feature flags.
Creating and Using Secrets
Creating Secrets
# Create a secret from a string
echo "my-super-secret-password" | docker secret create db_password -
# Create a secret from a file
docker secret create tls_cert ./server.crt
docker secret create tls_key ./server.key
# Create a secret from a generated password
openssl rand -base64 32 | docker secret create api_key -
# List secrets (only metadata, never the value)
docker secret ls
# Inspect secret metadata
docker secret inspect db_password
docker secret inspect shows only metadata (ID, name, creation date), never the content. Keep a secure backup of your secret values in a password manager or vault.
Using Secrets in Services
# Grant a service access to a secret
docker service create \
--name postgres \
--secret db_password \
--env POSTGRES_PASSWORD_FILE=/run/secrets/db_password \
postgres:16
# Grant with a custom target path and permissions
docker service create \
--name api \
--secret source=tls_cert,target=/app/certs/server.crt,mode=0444 \
--secret source=tls_key,target=/app/certs/server.key,mode=0400 \
--secret db_password \
myapp/api:latest
Inside the container, secrets appear as files:
# Default mount point
$ cat /run/secrets/db_password
my-super-secret-password
# Custom target
$ ls -la /app/certs/
-r--r--r-- 1 root root 1234 Apr 4 12:00 server.crt
-r-------- 1 root root 567 Apr 4 12:00 server.key
Using Secrets in Stack Files
version: "3.8"
services:
api:
image: myapp/api:v2.1.0
secrets:
- db_password
- api_key
- source: tls_cert
target: /app/certs/server.crt
mode: 0444
- source: tls_key
target: /app/certs/server.key
mode: 0400
environment:
DB_PASSWORD_FILE: /run/secrets/db_password
API_KEY_FILE: /run/secrets/api_key
postgres:
image: postgres:16
secrets:
- db_password
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
db_password:
external: true
api_key:
external: true
tls_cert:
file: ./certs/server.crt
tls_key:
file: ./certs/server.key
_FILE environment variable suffix convention. Instead of POSTGRES_PASSWORD=mypass, set POSTGRES_PASSWORD_FILE=/run/secrets/db_password and the image will read the password from the file. Check the image documentation for support.
Reading Secrets in Your Application
If your application does not natively support file-based secrets, you need a small wrapper. Here are patterns for common languages:
# Python: read secret from file with fallback to env var
import os
def get_secret(name):
"""Read a Docker secret, falling back to environment variable."""
secret_path = f"/run/secrets/{name}"
if os.path.isfile(secret_path):
with open(secret_path, 'r') as f:
return f.read().strip()
return os.environ.get(name.upper())
db_password = get_secret("db_password")
api_key = get_secret("api_key")
// Go: read secret from file with fallback
func getSecret(name string) (string, error) {
path := filepath.Join("/run/secrets", name)
data, err := os.ReadFile(path)
if err == nil {
return strings.TrimSpace(string(data)), nil
}
if val, ok := os.LookupEnv(strings.ToUpper(name)); ok {
return val, nil
}
return "", fmt.Errorf("secret %s not found", name)
}
// Node.js: read secret from file with fallback
const fs = require('fs');
const path = require('path');
function getSecret(name) {
const secretPath = path.join('/run/secrets', name);
try {
return fs.readFileSync(secretPath, 'utf8').trim();
} catch {
return process.env[name.toUpperCase()];
}
}
const dbPassword = getSecret('db_password');
Rotating Secrets
Swarm secrets are immutable. You cannot update a secret's value. Instead, you create a new version and rotate the service to use it. This is by design: immutability ensures that every task gets the same value and there is no race condition during updates.
The Rotation Pattern
# Step 1: Create the new secret with a version suffix
echo "new-password-value" | docker secret create db_password_v2 -
# Step 2: Update the service to use the new secret
docker service update \
--secret-rm db_password \
--secret-add source=db_password_v2,target=db_password \
myapp_api
# Step 3: Verify the service is running with the new secret
docker service ps myapp_api
# Step 4: Remove the old secret (only after all services are updated)
docker secret rm db_password
# Step 5: Optionally rename for consistency (create alias)
# Not natively supported; use naming conventions instead
Automated Rotation Script
#!/bin/bash
# rotate-secret.sh - Rotate a Docker Swarm secret
set -euo pipefail
SECRET_NAME="$1"
NEW_VALUE="$2"
SERVICES="$3" # Comma-separated list of services using this secret
TIMESTAMP=$(date +%Y%m%d%H%M%S)
NEW_SECRET="${SECRET_NAME}_${TIMESTAMP}"
echo "Creating new secret: $NEW_SECRET"
echo "$NEW_VALUE" | docker secret create "$NEW_SECRET" -
for service in $(echo "$SERVICES" | tr ',' '\n'); do
echo "Rotating secret for service: $service"
docker service update \
--secret-rm "$SECRET_NAME" \
--secret-add "source=${NEW_SECRET},target=${SECRET_NAME}" \
"$service" \
--detach=false
done
echo "Rotation complete. Old secret can be removed after verification."
echo "Run: docker secret rm $SECRET_NAME"
Rotation in Stack Files
For stack-based deployments, the cleanest rotation pattern uses versioned secret names:
version: "3.8"
services:
api:
image: myapp/api:v2.1.0
secrets:
- source: db_password
target: db_password # Application always reads same path
secrets:
db_password:
# Change this name to trigger rotation on stack deploy
name: db_password_v3
external: true
# Create the new versioned secret
echo "new-password" | docker secret create db_password_v3 -
# Update the stack file to reference v3, then redeploy
docker stack deploy -c docker-compose.yml myapp
Config Objects
Configs work identically to secrets in terms of distribution but are designed for non-sensitive data. They are not encrypted at rest and their content is visible via docker config inspect.
# Create a config from a file
docker config create nginx_conf ./nginx.conf
docker config create app_config ./config.yaml
# Create a config from stdin
echo '{"feature_flags": {"dark_mode": true}}' | \
docker config create feature_flags -
# List configs
docker config ls
# Inspect config (shows full content)
docker config inspect --pretty app_config
Using Configs in Services
# Mount config as a file
docker service create \
--name nginx \
--config source=nginx_conf,target=/etc/nginx/nginx.conf,mode=0444 \
nginx:latest
# In a stack file
version: "3.8"
services:
nginx:
image: nginx:latest
configs:
- source: nginx_conf
target: /etc/nginx/nginx.conf
mode: 0444
api:
image: myapp/api:latest
configs:
- source: app_config
target: /app/config.yaml
- source: feature_flags
target: /app/features.json
configs:
nginx_conf:
file: ./nginx/nginx.conf
app_config:
file: ./config/app.yaml
feature_flags:
external: true
Rotating Configs
Config rotation follows the same pattern as secrets:
# Create new version
docker config create nginx_conf_v2 ./nginx-updated.conf
# Update the service
docker service update \
--config-rm nginx_conf \
--config-add source=nginx_conf_v2,target=/etc/nginx/nginx.conf,mode=0444 \
myapp_nginx
# Clean up old config
docker config rm nginx_conf
HashiCorp Vault Integration
For organizations that need centralized secret management with audit logging, dynamic secrets, and fine-grained access control, integrating HashiCorp Vault with Docker Swarm is the recommended approach.
Pattern 1: Init Container Approach
Use an entrypoint script that fetches secrets from Vault before starting the application:
# Dockerfile
FROM myapp/api:latest
COPY vault-entrypoint.sh /vault-entrypoint.sh
RUN chmod +x /vault-entrypoint.sh
ENTRYPOINT ["/vault-entrypoint.sh"]
CMD ["./api-server"]
#!/bin/bash
# vault-entrypoint.sh
set -euo pipefail
# Authenticate with Vault using AppRole
VAULT_TOKEN=$(curl -s \
--request POST \
--data "{\"role_id\":\"${VAULT_ROLE_ID}\",\"secret_id\":\"$(cat /run/secrets/vault_secret_id)\"}" \
"${VAULT_ADDR}/v1/auth/approle/login" | jq -r '.auth.client_token')
# Fetch secrets and write to files
curl -s -H "X-Vault-Token: ${VAULT_TOKEN}" \
"${VAULT_ADDR}/v1/secret/data/myapp/db" | \
jq -r '.data.data.password' > /run/secrets/db_password
curl -s -H "X-Vault-Token: ${VAULT_TOKEN}" \
"${VAULT_ADDR}/v1/secret/data/myapp/api" | \
jq -r '.data.data.key' > /run/secrets/api_key
# Execute the main application
exec "$@"
Pattern 2: Vault Agent Sidecar
version: "3.8"
services:
vault-agent:
image: hashicorp/vault:latest
command: agent -config=/vault/config.hcl
configs:
- source: vault_agent_config
target: /vault/config.hcl
volumes:
- shared-secrets:/vault/secrets
deploy:
mode: global
api:
image: myapp/api:latest
volumes:
- shared-secrets:/run/secrets:ro
depends_on:
- vault-agent
volumes:
shared-secrets:
driver: local
driver_opts:
type: tmpfs
device: tmpfs
o: size=10m
configs:
vault_agent_config:
file: ./vault/agent-config.hcl
Pattern 3: Sync Script
A cron-based approach that syncs Vault secrets into Docker Swarm secrets:
#!/bin/bash
# vault-to-swarm-sync.sh
set -euo pipefail
VAULT_ADDR="${VAULT_ADDR:-https://vault.internal:8200}"
SECRET_PATH="secret/data/myapp"
# Authenticate
export VAULT_TOKEN=$(vault login -method=approle \
role_id="$VAULT_ROLE_ID" \
secret_id="$VAULT_SECRET_ID" \
-format=json | jq -r '.auth.client_token')
# Fetch all secrets under the path
SECRETS=$(vault kv list -format=json "${SECRET_PATH}" | jq -r '.[]')
for secret_name in $SECRETS; do
value=$(vault kv get -field=value "${SECRET_PATH}/${secret_name}")
current_version=$(docker secret ls --filter "name=${secret_name}" \
--format '{{.Name}}' | sort -V | tail -1)
new_name="${secret_name}_$(date +%s)"
echo "$value" | docker secret create "$new_name" -
# Update services that use this secret
for service in $(docker service ls --format '{{.Name}}'); do
if docker service inspect "$service" --format '{{json .Spec.TaskTemplate.ContainerSpec.Secrets}}' | \
grep -q "\"${secret_name}\""; then
docker service update \
--secret-rm "$current_version" \
--secret-add "source=${new_name},target=${secret_name}" \
"$service" --detach
fi
done
done
Best Practices
- Never put secrets in environment variables. They appear in
docker inspect, process listings (/proc/*/environ), and often end up in logs. Use Swarm secrets mounted as files. - Version your secrets. Use a naming convention like
secret_name_v1,secret_name_v2. This creates an audit trail and allows atomic rotation. - Use
external: truein stack files. This separates secret creation (a manual or automated security operation) from service deployment. Secrets should exist before the stack is deployed. - Set restrictive file modes. Mount secrets with
mode: 0400(owner read only) unless the application requires broader permissions. - Do not log secret file contents. Ensure your application never logs the values it reads from
/run/secrets/. - Clean up unused secrets. After rotation, old secret versions pile up. Schedule regular cleanup.
- Separate secrets per environment. Use naming prefixes:
prod_db_password,staging_db_password. Never share secrets across environments. - Back up your secret values externally. Swarm secrets are stored in the Raft log. If you lose all manager nodes, you lose all secrets. Keep an encrypted backup in a password manager or Vault.
Secret Cleanup
# List all secrets
docker secret ls
# Find orphaned secrets (not used by any service)
for secret in $(docker secret ls -q); do
name=$(docker secret inspect "$secret" --format '{{.Spec.Name}}')
used=false
for service in $(docker service ls -q); do
if docker service inspect "$service" \
--format '{{json .Spec.TaskTemplate.ContainerSpec.Secrets}}' 2>/dev/null | \
grep -q "\"$name\""; then
used=true
break
fi
done
if [ "$used" = false ]; then
echo "ORPHANED: $name"
fi
done
# Remove orphaned secrets (after verification)
docker secret rm secret_name_v1 secret_name_v2
Conclusion
Docker Swarm's secrets and configs are underappreciated features. They solve a real security problem (credential distribution) without requiring external infrastructure. For small-to-medium deployments, Swarm secrets alone may be sufficient. For larger organizations with compliance requirements, combine Swarm secrets with HashiCorp Vault for centralized audit logging and dynamic secret generation.
The key patterns to remember: secrets are immutable (create new, rotate, delete old), always use file-based secrets over environment variables, and version everything. With these practices in place, your Swarm cluster handles sensitive data safely without the operational complexity of bolting on external secret managers from day one.