Managing Secrets
Secrets like API keys, database passwords, and certificates require special handling in containerized applications. This lesson covers secure approaches to secret management.
The Secret Management Challenge
Secrets are sensitive data that should:
- Never be in source control
- Never be baked into images
- Be encrypted at rest and in transit
- Have limited access and rotation capability
┌─────────────────────────────────────────────────────────────────┐
│ Common Mistakes │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ❌ Secrets in Dockerfile │
│ ENV API_KEY=abc123 │
│ │
│ ❌ Secrets in source code │
│ const password = "secret123" │
│ │
│ ❌ Secrets in docker-compose.yml │
│ environment: │
│ DB_PASSWORD: mysecretpassword │
│ │
│ ❌ Secrets committed to git │
│ .env with real credentials │
│ │
└─────────────────────────────────────────────────────────────────┘
Environment Variables (Basic Approach)
For development and simple deployments:
# Pass at runtime (not in image)
docker run \
-e API_KEY=$API_KEY \
-e DB_PASSWORD=$DB_PASSWORD \
myapp
# Or from env file
docker run --env-file .env.local myapp
Limitations of Env Vars
- Visible in
docker inspect - May appear in logs
- Accessible to all processes in container
- Limited size
# Anyone with Docker access can see secrets
docker inspect -f '{{json .Config.Env}}' mycontainer
Docker Secrets (Swarm Mode)
Docker Swarm provides built-in secret management:
# Create a secret
echo "mysupersecretpassword" | docker secret create db_password -
# Or from file
docker secret create db_password ./password.txt
# List secrets
docker secret ls
# Use in service
docker service create \
--name api \
--secret db_password \
myapi
Accessing Secrets in Container
Secrets are mounted as files in /run/secrets/:
# In container
cat /run/secrets/db_password
// In application code
const fs = require('fs');
const dbPassword = fs.readFileSync('/run/secrets/db_password', 'utf8').trim();
Docker Compose with Secrets
# docker-compose.yml
services:
api:
image: myapi
secrets:
- db_password
- api_key
environment:
# Reference secret file location
DB_PASSWORD_FILE: /run/secrets/db_password
API_KEY_FILE: /run/secrets/api_key
secrets:
db_password:
file: ./secrets/db_password.txt
api_key:
file: ./secrets/api_key.txt
Reading Secrets in Application
// Helper function to read secrets
function getSecret(name) {
const envVar = process.env[name];
const fileVar = process.env[`${name}_FILE`];
if (fileVar) {
return fs.readFileSync(fileVar, 'utf8').trim();
}
return envVar;
}
// Usage
const dbPassword = getSecret('DB_PASSWORD');
External Secret Management
For production, use dedicated secret managers:
HashiCorp Vault
services:
api:
image: myapi
environment:
VAULT_ADDR: https://vault.example.com
VAULT_TOKEN: ${VAULT_TOKEN}
// Fetch secrets from Vault
const vault = require('node-vault')({
endpoint: process.env.VAULT_ADDR,
token: process.env.VAULT_TOKEN,
});
const secrets = await vault.read('secret/data/myapp');
const dbPassword = secrets.data.data.db_password;
AWS Secrets Manager
services:
api:
image: myapi
environment:
AWS_REGION: us-east-1
SECRET_NAME: myapp/production/db
const {
SecretsManagerClient,
GetSecretValueCommand,
} = require('@aws-sdk/client-secrets-manager');
const client = new SecretsManagerClient({ region: process.env.AWS_REGION });
const response = await client.send(
new GetSecretValueCommand({ SecretId: process.env.SECRET_NAME })
);
const secrets = JSON.parse(response.SecretString);
Build-Time Secrets (BuildKit)
For secrets needed during image build:
# syntax=docker/dockerfile:1.4
FROM node:18
# Mount secret during build (not stored in image)
RUN --mount=type=secret,id=npm_token \
NPM_TOKEN=$(cat /run/secrets/npm_token) \
npm install
# Build with secret
docker build --secret id=npm_token,src=.npm_token .
Pattern: Init Container
Fetch secrets before main application starts:
services:
secret-fetcher:
image: vault-agent
volumes:
- secrets:/secrets
command: >
vault read -field=password secret/myapp > /secrets/db_password
api:
image: myapi
volumes:
- secrets:/run/secrets:ro
depends_on:
secret-fetcher:
condition: service_completed_successfully
volumes:
secrets:
driver_opts:
type: tmpfs
device: tmpfs
Secret Rotation
Plan for secret rotation:
services:
api:
image: myapi
environment:
# Support both old and new during rotation
DB_PASSWORD: ${DB_PASSWORD}
DB_PASSWORD_NEW: ${DB_PASSWORD_NEW:-}
// Application handles rotation gracefully
const passwords = [
process.env.DB_PASSWORD,
process.env.DB_PASSWORD_NEW,
].filter(Boolean);
async function connect() {
for (const password of passwords) {
try {
return await db.connect({ password });
} catch (e) {
continue;
}
}
throw new Error('All database passwords failed');
}
Security Checklist
Do
- Use environment variables set at runtime
- Use Docker secrets or external secret managers
- Use BuildKit secrets for build-time secrets
- Encrypt secrets at rest
- Rotate secrets regularly
- Limit access with RBAC
Don't
- Commit secrets to git
- Hardcode secrets in Dockerfiles
- Log secrets
- Use secrets in image layer (even if deleted later)
- Share secrets across environments
Auditing Secret Access
services:
api:
image: myapi
logging:
driver: json-file
options:
max-size: "10m"
labels: "service,environment"
labels:
- "secrets.accessed=db_password,api_key"
Development vs Production
┌─────────────────────────────────────────────────────────────────┐
│ Development Production │
├─────────────────────────────────────────────────────────────────┤
│ │
│ .env.local (not committed) External Secret Manager │
│ (Vault, AWS SM, etc.) │
│ │
│ Docker Compose env_file Docker Secrets or │
│ Kubernetes Secrets │
│ │
│ Readable/simple Encrypted, audited, │
│ access-controlled │
│ │
└─────────────────────────────────────────────────────────────────┘
Development Setup
# docker-compose.yml
services:
api:
env_file:
- .env.development
# .env.development (committed, fake values)
DATABASE_URL=postgresql://dev:dev@db:5432/devdb
API_KEY=dev-api-key
# .env.local (not committed, real dev values)
DATABASE_URL=postgresql://real:password@db:5432/realdb
Production Setup
# docker-compose.prod.yml
services:
api:
secrets:
- db_password
- api_key
environment:
DATABASE_URL_BASE: postgresql://user:@db:5432/prod
DB_PASSWORD_FILE: /run/secrets/db_password
secrets:
db_password:
external: true
api_key:
external: true
Key Takeaways
- Never store secrets in images or source control
- Use environment variables for simple cases
- Docker secrets provide file-based secret injection
- External secret managers are best for production
- BuildKit secrets handle build-time credentials
- Plan for secret rotation from the start
- Different strategies for development vs production
- Always audit secret access

