Overview
Hardcoding secrets (passwords, API keys, tokens) in Docker images or Compose files is a critical security risk. Images are shared, cached, and inspected — anyone with access to the image can extract secrets. Below is the idiomatic way to secure patterns for injecting secrets into containers at runtime.
When to Use
- You need to pass database passwords, API keys, or TLS certificates to containers
- You want to avoid committing secrets to version control
- You use Docker Swarm or Compose in production
- You need to rotate secrets without rebuilding images
Solution
Docker Swarm secrets (most secure)
# Create a secret from a file
echo "my-super-secret-password" | docker secret create db_password -
# Create a secret from stdin
printf "AKIAIOSFODNN7EXAMPLE" | docker secret create aws_access_key -
# List secrets
docker secret ls
# Use in a Swarm service
docker service create \
--name api \
--secret db_password \
--secret aws_access_key \
-e DB_PASSWORD_FILE=/run/secrets/db_password \
-e AWS_KEY_FILE=/run/secrets/aws_access_key \
my-api:latest
Docker mounts secrets as files at /run/secrets/<secret_name>. They are never exposed as environment variables and are encrypted in transit.
Reading secrets from files in your app
import os
def get_secret(name: str) -> str:
"""Read a secret from a file (Docker Swarm pattern)."""
file_path = os.environ.get(f"{name}_FILE")
if file_path:
with open(file_path, "r") as f:
return f.read().strip()
# Fallback to env var for local dev
return os.environ.get(name, "")
const fs = require("fs");
function getSecret(name) {
const filePath = process.env[`${name}_FILE`];
if (filePath) {
return fs.readFileSync(filePath, "utf8").trim();
}
return process.env[name] || "";
}
Docker Compose with secrets
# docker-compose.yml
services:
api:
build: .
secrets:
- db_password
- api_key
environment:
- DB_PASSWORD_FILE=/run/secrets/db_password
- API_KEY_FILE=/run/secrets/api_key
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txt
api_key:
file: ./secrets/api_key.txt
.env file (development only)
# .env (NEVER commit this — add to .gitignore)
DB_PASSWORD=my-dev-password
API_KEY=dev-api-key-12345
JWT_SECRET=dev-jwt-secret
# docker-compose.dev.yml
services:
api:
build: .
env_file:
- .env
environment:
- NODE_ENV=development
# .gitignore
.env
.env.*
secrets/
External secret manager (HashiCorp Vault)
# docker-compose.yml
services:
api:
build: .
environment:
- VAULT_ADDR=https://vault.internal:8200
- VAULT_TOKEN_FILE=/run/secrets/vault_token
secrets:
- vault_token
command: ["./wait-for-vault.sh", "node", "server.js"]
vault:
image: hashicorp/vault:1.15
ports:
- "8200:8200"
environment:
VAULT_DEV_ROOT_TOKEN_ID: root
cap_add:
- IPC_LOCK
secrets:
vault_token:
file: ./secrets/vault_token.txt
import hvac
def get_vault_secret(path: str) -> dict:
"""Fetch a secret from HashiCorp Vault."""
client = hvac.Client(
url=os.environ["VAULT_ADDR"],
token=get_secret("VAULT_TOKEN")
)
result = client.secrets.kv.v2.read_secret_version(path=path)
return result["data"]["data"]
Build-time secrets with BuildKit
# Dockerfile
# syntax=docker/dockerfile:1.6
FROM node:20-alpine
# Mount secret during build only — not stored in image layers
RUN --mount=type=secret,id=npm_token \
npm config set //registry.npmjs.org/:_authToken=$(cat /run/secrets/npm_token) && \
npm ci --omit=dev && \
npm config delete //registry.npmjs.org/:_authToken
# Build with BuildKit secret
docker build --secret id=npm_token,source=$HOME/.npmrc -t my-api .
Runtime secrets with environment variables (less secure)
# docker-compose.prod.yml
services:
api:
build: .
environment:
- DB_PASSWORD=${DB_PASSWORD} # From --env-file or shell
# Pass via shell (not visible in docker inspect after Compose v2)
export DB_PASSWORD=strong-prod-password
docker compose --env-file .env.prod up -d
Explanation
Secret management patterns ranked by security:
- Docker Swarm secrets: Secrets are encrypted at rest and in transit. Mounted as read-only files at
/run/secrets/. Never appear in environment variables ordocker inspect. Best for Swarm deployments. - BuildKit mount secrets: Secrets are available during build but not stored in image layers. The secret file is mounted temporarily and removed after the RUN command. Best for private npm/pip registries.
- External secret managers (Vault, AWS Secrets Manager): Secrets are fetched at runtime from a central vault. Supports rotation, audit logging, and fine-grained access control. Best for enterprise production.
- .env files: Simple but risky. Secrets are in plaintext on disk. Use only for development. Never commit to version control.
- Environment variables: Visible in
docker inspectanddocker exec env. Least secure for production. Use only for non-sensitive configuration.
Key principles:
- Secrets should never be baked into image layers.
ENVandARGvalues are visible in image history. - The
_FILEsuffix convention tells your app to read the secret from a file path instead of an environment variable value. - Rotate secrets by updating the secret in the manager, not by rebuilding images.
Variants
| Method | Security | Complexity | Use When |
|---|---|---|---|
| Swarm secrets | High | Low | Docker Swarm production |
| BuildKit mount | High | Medium | Private registries during build |
| Vault / Secrets Manager | High | High | Enterprise, rotation needed |
| .env file | Low | Low | Development only |
| Environment variables | Low | Low | Non-sensitive config |
Guidelines
- Never hardcode secrets in Dockerfiles (
ENV,ARG) or Compose files. - Use Docker Swarm secrets for Swarm deployments.
- Use BuildKit
--mount=type=secretfor build-time credentials (npm, pip, apt). - Read secrets from files using the
_FILEsuffix convention. - Add
.envandsecrets/to.gitignore. - Use external secret managers (Vault, AWS Secrets Manager) for enterprise production.
- Rotate secrets regularly without rebuilding images.
- Limit secret access to services that need them.
- Audit secret access with
docker secret lsand Vault audit logs.
Common Mistakes
- Using
ENVin Dockerfiles for secrets. These are visible indocker historyand image inspection. - Committing
.envfiles to Git. Always add to.gitignore. - Passing secrets as command-line arguments. They appear in
docker inspectand process listings. - Not using the
_FILEsuffix convention. Apps that only read env vars cannot use Swarm secrets. - Giving every service access to every secret. Follow least privilege.
- Not rotating secrets. Compromised secrets should be replaceable without downtime.
- Using the same secret across environments. Dev, staging, and prod should have different secrets.
Performance Tips
- Cache secret lookups in memory. Avoid fetching the same secret on every request:
_secret_cache = {}
def get_cached_secret(name: str, ttl: int = 300) -> str:
if name in _secret_cache:
cached_time, value = _secret_cache[name]
if time.time() - cached_time < ttl:
return value
value = get_secret(name)
_secret_cache[name] = (time.time(), value)
return value
- Use sidecar pattern for secret fetching. A sidecar container fetches secrets and writes them to a shared volume:
services:
secret-fetcher:
image: vault-sidecar:latest
volumes:
- secrets:/run/secrets
api:
image: my-api:latest
volumes:
- secrets:/run/secrets:ro
volumes:
secrets:
- Batch secret fetches from Vault. Reduce API calls by fetching multiple secrets in one request:
def get_multiple_secrets(paths: list[str]) -> dict:
results = {}
for path in paths:
results[path] = get_vault_secret(path)
return results Frequently Asked Questions
Are Docker Compose secrets as secure as Swarm secrets?
No. Compose secrets are mounted as files from the host filesystem. They are not encrypted at rest. Swarm secrets are encrypted and managed by the Swarm manager. Use Compose secrets for development and Swarm secrets for production.
Can I use Docker secrets without Swarm?
Docker Compose supports secrets via the secrets key with file: source. This mounts the file into the container. It is less secure than Swarm secrets but better than environment variables.
How do I rotate secrets without downtime?
In Swarm, update the secret and then update the service: docker service update --secret-rm db_password --secret-add db_password=db_password_v2 api. The service restarts with the new secret. For zero-downtime, use rolling updates.
Why should I avoid environment variables for secrets?
Environment variables are visible in docker inspect, docker exec env, and /proc/<pid>/environ on the host. They can leak into logs and crash dumps. File-based secrets are more secure because they are only readable by the container process.
AWS Secrets Manager Integration
import boto3
import json
def get_aws_secret(secret_name: str, region: str = "us-east-1") -> dict:
"""Fetch a secret from AWS Secrets Manager."""
client = boto3.client("secretsmanager", region_name=region)
response = client.get_secret_value(SecretId=secret_name)
return json.loads(response["SecretString"])
# Usage
db_creds = get_aws_secret("prod/db/credentials")
db_password = db_creds["password"]
# docker-compose.yml with AWS Secrets Manager
services:
api:
build: .
environment:
- AWS_REGION=us-east-1
- AWS_SECRET_NAME=prod/db/credentials
# AWS credentials via IAM role, not env vars
Kubernetes Secrets (alternative to Docker Swarm)
# k8s-secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
type: Opaque
stringData:
password: my-super-secret-password
username: dbadmin
# k8s-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
template:
spec:
containers:
- name: api
image: my-api:latest
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
# Or mount as file
volumeMounts:
- name: secrets
mountPath: /run/secrets
readOnly: true
volumes:
- name: secrets
secret:
secretName: db-credentials
Automated Secret Rotation Script
#!/bin/bash
# rotate-secrets.sh
set -e
SECRET_NAME="db_password"
NEW_VALUE=$(openssl rand -base64 32)
# Update in Docker Swarm
echo "$NEW_VALUE" | docker secret create "${SECRET_NAME}_v2" -
# Update service to use new secret
docker service update \
--secret-rm "$SECRET_NAME" \
--secret-add "${SECRET_NAME}_v2" \
api
# Wait for rollout
sleep 30
# Remove old secret
docker secret rm "$SECRET_NAME"
echo "Rotation complete for $SECRET_NAME"
Related Resources
Docker Network Isolation and Inter-Container Security
Secure inter-container communication with custom Docker networks, network segmentation, and access control policies.
RecipeDocker Health Check Configuration for Container Reliability
Add proper health checks to Docker containers with HEALTHCHECK
RecipeDocker Compose Dev/Prod Split: Separate Environments
Separate development and production Docker Compose configs with overrides
GuideWebhook Security — Delivery, Verification, and Protection
A practical guide to securing webhooks: signature verification, replay attack prevention, payload encryption, and endpoint hardening for reliable delivery.
PatternSidecar Pattern: Extend Services with Companion Containers
How to extend services with companion containers for cross-cutting concerns. Covers sidecar containers, shared volumes, health probes, and service mesh sidecars.
DocSecrets Rotation Template
A template for scheduling and tracking the rotation of API keys, tokens, and certificates.