Docker Secrets Management Guide
Audience: DevOps engineers, developers handling sensitive data
WHAT
Guide to managing secrets in Docker across BuildKit, Swarm, Compose, and Kubernetes.
WHY
Environment variables leak secrets in logs and process tables. Proper secrets management keeps credentials out of images, logs, and runtime inspection.
HOW
Note: The
versionkey is optional in modern Docker Compose and is omitted from examples below.
Table of Contents
- Overview
- Why Use Secrets Instead of Environment Variables
- Docker BuildKit Secrets (Build-time)
- Docker Swarm Secrets (Runtime)
- Docker Compose Secrets
- Kubernetes Secrets
- Application Integration
- Security Best Practices
- Migration from Environment Variables
Overview
Docker secrets provide a secure way to manage sensitive data such as passwords, API keys, SSH keys, certificates, and other credentials. Unlike environment variables, secrets are encrypted at rest and in transit, and are only accessible to services that need them.
Why Use Secrets Instead of Environment Variables
Environment Variables Limitations
- Visible in container inspect output
- Logged in process listings (
ps aux) - Inherited by child processes
- Often accidentally logged
- Stored in plain text in compose files
Secrets Advantages
- Encrypted at rest
- Encrypted in transit
- Only accessible to authorized services
- Mounted as files (not in environment)
- Automatic rotation support
- Audit trail capabilities
- Override environment variables (including .env files)
Docker BuildKit Secrets (Build-time)
BuildKit secrets allow you to pass sensitive data during image build without leaving traces in the final image.
Basic BuildKit Secret Usage
# syntax=docker/dockerfile:1
FROM alpine:latest
# Mount secret during build only
RUN --mount=type=secret,id=github_token \
TOKEN=$(cat /run/secrets/github_token) && \
git clone https://${TOKEN}@github.com/private/repo.git /app
# Secret is NOT in the final image!
Build command:
# Pass secret from file
docker build --secret id=github_token,src=.secrets/github_token .
# Pass secret from environment variable
export GITHUB_TOKEN="ghp_xxxxxxxxxxxx"
echo $GITHUB_TOKEN | docker build --secret id=github_token -
Multiple BuildKit Secrets
# syntax=docker/dockerfile:1
FROM node:20-alpine
WORKDIR /app
# Use multiple secrets for private npm registry
RUN --mount=type=secret,id=npm_token \
--mount=type=secret,id=npm_registry \
NPM_TOKEN=$(cat /run/secrets/npm_token) && \
REGISTRY=$(cat /run/secrets/npm_registry) && \
echo "//${REGISTRY}/:_authToken=${NPM_TOKEN}" > ~/.npmrc && \
npm install && \
rm ~/.npmrc # Clean up
# No secrets in the image layers!
SSH Key for Private Repositories
# syntax=docker/dockerfile:1
FROM golang:1.21-alpine
# Use SSH key to clone private repos
RUN --mount=type=ssh \
mkdir -p -m 0700 ~/.ssh && \
ssh-keyscan github.com >> ~/.ssh/known_hosts && \
git clone git@github.com:private/repo.git /app
# Alternative: Using secret file
RUN --mount=type=secret,id=ssh_key,target=/root/.ssh/id_rsa,mode=0600 \
--mount=type=secret,id=known_hosts,target=/root/.ssh/known_hosts \
git clone git@github.com:private/repo.git /app
Build with SSH:
docker build --ssh default .
# or with specific key
docker build --ssh default=$HOME/.ssh/id_rsa .
Docker Swarm Secrets (Runtime)
Docker Swarm mode provides built-in secret management for runtime secrets.
Creating Swarm Secrets
# From string
echo "MyP@ssw0rd" | docker secret create db_password -
# From file
docker secret create ssl_cert cert.pem
# Generate random secret
openssl rand -base64 32 | docker secret create api_key -
Using Secrets in Swarm Services
# docker-compose.yml (Swarm mode)
services:
database:
image: postgres:16
secrets:
- db_password
- db_root_password
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
POSTGRES_ROOT_PASSWORD_FILE: /run/secrets/db_root_password
deploy:
replicas: 1
app:
image: myapp:latest
secrets:
- db_password
- api_key
- jwt_secret
environment:
# Reference secret files
DATABASE_PASSWORD_FILE: /run/secrets/db_password
API_KEY_FILE: /run/secrets/api_key
JWT_SECRET_FILE: /run/secrets/jwt_secret
secrets:
db_password:
external: true # Created with docker secret create
db_root_password:
external: true
api_key:
external: true
jwt_secret:
external: true
Deploy to Swarm:
docker stack deploy -c docker-compose.yml myapp
Secret Rotation in Swarm
# Create new version
echo "NewP@ssw0rd" | docker secret create db_password_v2 -
# Update service to use new secret
docker service update \
--secret-rm db_password \
--secret-add source=db_password_v2,target=db_password \
myapp_database
Secrets vs Environment Variables Precedence
Configuration Precedence Order
When using both secrets and environment variables, the precedence depends on how your application reads configuration:
// Application-controlled precedence
function getConfig(name) {
// 1. HIGHEST PRIORITY: Secret file
const secretFile = process.env[`${name}_FILE`];
if (secretFile && fs.existsSync(secretFile)) {
return fs.readFileSync(secretFile, 'utf8').trim();
}
// 2. FALLBACK: Environment variable
return process.env[name];
}
Overriding .env with Secrets
# docker-compose.yml
services:
app:
image: myapp:latest
env_file:
- .env # Contains: PASSWORD=development
secrets:
- password # Contains: production-secret
environment:
# This makes the app use secret instead of .env
PASSWORD_FILE: /run/secrets/password
# Fallback from .env if secret doesn't exist
PASSWORD: ${PASSWORD}
secrets:
password:
file: ./secrets/password # OVERRIDES .env value!
Practical Override Example
# .env (development defaults)
DATABASE_PASSWORD=localdev
REDIS_PASSWORD=redis123
API_KEY=dev-key
# docker-compose.yml
services:
app:
env_file: .env
secrets:
- db_password # Overrides DATABASE_PASSWORD
- redis_password # Overrides REDIS_PASSWORD
# api_key not provided as secret, uses .env value
environment:
# Secrets override .env for sensitive data
DATABASE_PASSWORD_FILE: /run/secrets/db_password
REDIS_PASSWORD_FILE: /run/secrets/redis_password
# API_KEY from .env (no secret override)
API_KEY: ${API_KEY}
secrets:
db_password:
file: ./secrets/db_password # production value
redis_password:
file: ./secrets/redis_password # production value
Result:
DATABASE_PASSWORD: Uses secret (production value)REDIS_PASSWORD: Uses secret (production value)API_KEY: Uses .env value (dev-key)
Docker Compose Secrets
Docker Compose (non-Swarm) supports secrets for local development.
File-based Secrets (Development)
# docker-compose.yml
services:
database:
image: postgres:16
secrets:
- db_password
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
app:
build: .
secrets:
- db_password
- api_key
environment:
DATABASE_PASSWORD_FILE: /run/secrets/db_password
API_KEY_FILE: /run/secrets/api_key
secrets:
db_password:
file: ./secrets/db_password.txt # Local file
api_key:
file: ./secrets/api_key.txt
Directory structure:
project/
├── docker-compose.yml
├── secrets/ # Add to .gitignore!
│ ├── db_password.txt
│ └── api_key.txt
└── .gitignore
Environment Variable Secrets (CI/CD)
# docker-compose.yml
services:
app:
image: myapp:latest
secrets:
- db_password
- api_key
environment:
DATABASE_PASSWORD_FILE: /run/secrets/db_password
API_KEY_FILE: /run/secrets/api_key
secrets:
db_password:
environment: DB_PASSWORD_SECRET # From env var
api_key:
environment: API_KEY_SECRET
Usage:
export DB_PASSWORD_SECRET="production-password"
export API_KEY_SECRET="sk-prod-key"
docker-compose up
Kubernetes Secrets
Kubernetes has its own secret management system.
Creating Kubernetes Secrets
# From literals
kubectl create secret generic app-secrets \
--from-literal=db-password='MyP@ssw0rd' \
--from-literal=api-key='sk-proj-xxxxx'
# From files
kubectl create secret generic app-secrets \
--from-file=ssh-key=/path/to/id_rsa \
--from-file=ssl-cert=/path/to/cert.pem
# From .env file
kubectl create secret generic app-secrets \
--from-env-file=.env.production
Using Secrets in Pods
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
template:
spec:
containers:
- name: app
image: myapp:latest
# Mount as files
volumeMounts:
- name: secrets
mountPath: /run/secrets
readOnly: true
# Or as environment variables (less secure)
env:
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: app-secrets
key: db-password
volumes:
- name: secrets
secret:
secretName: app-secrets
# Set file permissions
defaultMode: 0400
items:
- key: db-password
path: db_password
- key: api-key
path: api_key
Sealed Secrets (GitOps)
For GitOps workflows, use Sealed Secrets to encrypt secrets that can be stored in Git:
# Install sealed-secrets controller
kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.24.0/controller.yaml
# Create a secret
echo -n "MyP@ssw0rd" | kubectl create secret generic db-secret \
--dry-run=client \
--from-file=password=/dev/stdin \
-o yaml | kubeseal -o yaml > sealed-secret.yaml
# Commit sealed-secret.yaml to Git (safe!)
git add sealed-secret.yaml
git commit -m "Add encrypted database password"
Application Integration
Reading Secrets from Files (Recommended)
Node.js Example:
const fs = require('fs');
function getSecret(secretName) {
const secretPath = process.env[`${secretName}_FILE`];
if (secretPath) {
try {
return fs.readFileSync(secretPath, 'utf8').trim();
} catch (err) {
console.error(`Failed to read secret ${secretName}:`, err);
throw err;
}
}
// Fallback to env var for development
return process.env[secretName];
}
// Usage
const dbPassword = getSecret('DATABASE_PASSWORD');
const apiKey = getSecret('API_KEY');
Python Example:
import os
from pathlib import Path
def get_secret(secret_name):
"""Read secret from file or environment variable."""
secret_file = os.environ.get(f"{secret_name}_FILE")
if secret_file:
try:
return Path(secret_file).read_text().strip()
except Exception as e:
print(f"Failed to read secret {secret_name}: {e}")
raise
# Fallback to environment variable
return os.environ.get(secret_name)
# Usage
db_password = get_secret("DATABASE_PASSWORD")
api_key = get_secret("API_KEY")
Go Example:
package main
import (
"fmt"
"os"
"strings"
)
func GetSecret(name string) (string, error) {
// Check for file path
if filePath := os.Getenv(name + "_FILE"); filePath != "" {
data, err := os.ReadFile(filePath)
if err != nil {
return "", fmt.Errorf("reading secret %s: %w", name, err)
}
return strings.TrimSpace(string(data)), nil
}
// Fallback to environment variable
return os.Getenv(name), nil
}
// Usage
dbPassword, _ := GetSecret("DATABASE_PASSWORD")
apiKey, _ := GetSecret("API_KEY")
Shell Script Integration
#!/bin/bash
# Function to read secret
get_secret() {
local secret_name=$1
local file_var="${secret_name}_FILE"
if [ -n "${!file_var}" ]; then
# Read from file
cat "${!file_var}" 2>/dev/null | tr -d '\n'
else
# Fallback to env var
echo "${!secret_name}"
fi
}
# Usage
DB_PASSWORD=$(get_secret "DATABASE_PASSWORD")
API_KEY=$(get_secret "API_KEY")
# Connect to database
psql "postgresql://user:${DB_PASSWORD}@localhost/mydb"
Security Best Practices
1. Never Log Secrets
// BAD
console.log(`Connecting with password: ${password}`);
// GOOD
console.log('Connecting to database...');
2. Use Least Privilege
# Give each service only the secrets it needs
services:
frontend:
secrets:
- api_key # Only needs API key
backend:
secrets:
- db_password # Only needs database
- api_key
3. Rotate Secrets Regularly
# Automated rotation script
#!/bin/bash
NEW_PASSWORD=$(openssl rand -base64 32)
echo "${NEW_PASSWORD}" | docker secret create "db_password_$(date +%s)" -
docker service update --secret-rm db_password --secret-add db_password_new app
4. Encrypt Secrets at Rest
# docker-compose.yml with encrypted secrets
secrets:
db_password:
file: ./secrets/db_password.enc
driver: encrypted # Custom driver
5. Audit Secret Access
# Monitor secret access
docker events --filter event=secret
6. Use Short-Lived Tokens
# BuildKit with temporary token
RUN --mount=type=secret,id=temp_token \
TOKEN=$(cat /run/secrets/temp_token) && \
# Token expires in 5 minutes
curl -H "Authorization: Bearer $TOKEN" https://api.example.com/data
Migration from Environment Variables
Step 1: Identify Sensitive Variables
# Find potential secrets in docker-compose.yml
grep -E "(PASSWORD|SECRET|KEY|TOKEN|CERT)" docker-compose.yml
Step 2: Create Secret Files
# Create secrets directory
mkdir -p secrets
chmod 700 secrets
# Move secrets to files
echo "${DB_PASSWORD}" > secrets/db_password
echo "${API_KEY}" > secrets/api_key
chmod 600 secrets/*
Step 3: Update Docker Compose
# Before (environment variables)
services:
app:
environment:
- DB_PASSWORD=MyP@ssw0rd # BAD!
- API_KEY=sk-xxxxx # BAD!
# After (secrets)
services:
app:
secrets:
- db_password
- api_key
environment:
- DB_PASSWORD_FILE=/run/secrets/db_password
- API_KEY_FILE=/run/secrets/api_key
secrets:
db_password:
file: ./secrets/db_password
api_key:
file: ./secrets/api_key
Step 4: Update Application Code
// Before
const password = process.env.DB_PASSWORD;
// After
const password = fs.readFileSync(
process.env.DB_PASSWORD_FILE || '/run/secrets/db_password',
'utf8'
).trim();
Step 5: Update CI/CD Pipeline
# GitHub Actions example
- name: Deploy with secrets
run: |
echo "$" > db_password
echo "$" > api_key
docker stack deploy -c docker-compose.yml app
rm db_password api_key # Clean up
Examples
Complete Example: Secure Node.js App
Dockerfile:
# syntax=docker/dockerfile:1
FROM node:20-alpine
WORKDIR /app
# Use BuildKit secret for private npm packages
RUN --mount=type=secret,id=npm_token \
echo "//registry.npmjs.org/:_authToken=$(cat /run/secrets/npm_token)" > ~/.npmrc && \
npm ci --production && \
rm ~/.npmrc
COPY . .
USER node
EXPOSE 3000
CMD ["node", "server.js"]
docker-compose.yml:
services:
postgres:
image: postgres:16
secrets:
- db_password
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
POSTGRES_DB: myapp
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
secrets:
- redis_password
command: >
sh -c 'redis-server --requirepass "$$(cat /run/secrets/redis_password)"'
app:
build:
context: .
secrets:
- id: npm_token
src: ./.secrets/npm_token
secrets:
- db_password
- redis_password
- jwt_secret
- api_key
environment:
NODE_ENV: production
DATABASE_PASSWORD_FILE: /run/secrets/db_password
REDIS_PASSWORD_FILE: /run/secrets/redis_password
JWT_SECRET_FILE: /run/secrets/jwt_secret
API_KEY_FILE: /run/secrets/api_key
depends_on:
- postgres
- redis
ports:
- "3000:3000"
secrets:
db_password:
file: ./secrets/db_password
redis_password:
file: ./secrets/redis_password
jwt_secret:
file: ./secrets/jwt_secret
api_key:
file: ./secrets/api_key
volumes:
postgres_data:
Application Code (server.js):
const fs = require('fs');
const express = require('express');
const { Pool } = require('pg');
const redis = require('redis');
const jwt = require('jsonwebtoken');
// Secret helper
function getSecret(name) {
const filePath = process.env[`${name}_FILE`];
if (filePath && fs.existsSync(filePath)) {
return fs.readFileSync(filePath, 'utf8').trim();
}
throw new Error(`Secret ${name} not found`);
}
// Load secrets
const secrets = {
dbPassword: getSecret('DATABASE_PASSWORD'),
redisPassword: getSecret('REDIS_PASSWORD'),
jwtSecret: getSecret('JWT_SECRET'),
apiKey: getSecret('API_KEY')
};
// Database connection
const pool = new Pool({
host: 'postgres',
database: 'myapp',
user: 'postgres',
password: secrets.dbPassword
});
// Redis connection
const redisClient = redis.createClient({
host: 'redis',
password: secrets.redisPassword
});
// Express app
const app = express();
app.get('/health', (req, res) => {
res.json({ status: 'healthy' });
});
app.listen(3000, () => {
console.log('Server running on port 3000');
// Never log secrets!
console.log('Secrets loaded successfully');
});
Summary
When to Use Each Method
| Method | Use Case | Security Level |
|---|---|---|
| Environment Variables | Non-sensitive config | Low |
| BuildKit Secrets | Build-time secrets | High (build only) |
| Docker Swarm Secrets | Production runtime | High |
| Compose Secrets (file) | Development/testing | Medium |
| Kubernetes Secrets | K8s deployments | High |
Key Takeaways
✅ DO:
- Use secrets for all sensitive data
- Read secrets from files, not environment variables
- Rotate secrets regularly
- Use BuildKit secrets for build-time needs
- Implement proper secret management in production
❌ DON’T:
- Store secrets in environment variables
- Commit secret files to Git
- Log secret values
- Share secrets between unrelated services
- Leave secrets in Docker image layers
By following these patterns, your applications will have enterprise-grade secret management with proper security controls.