Managing Secrets and Configuration with Docker Compose
How to keep sensitive data safe in a Docker Compose setup. A practical approach to secrets, environment variables, and config management.
Jean-Pierre Broeders
Freelance DevOps Engineer
Managing Secrets and Configuration with Docker Compose
Somewhere on a production server, there's a .env file with database passwords in plain text. Sound familiar? It happens more often than anyone admits. Docker Compose makes spinning things up easy, but that convenience comes at a cost: sensitive configuration ends up in places it shouldn't be.
The Problem with .env Files
The default approach is straightforward. Throw everything in a .env, reference it from docker-compose.yml, done. Works great for development. Production is a different story.
First issue: .env files get accidentally committed. Yes, .gitignore exists. No, not everyone configures it properly. A quick git add . and suddenly it's on GitHub. Sometimes in a public repo.
Second: all secrets sit together. Database credentials next to SMTP config next to third-party API keys. One leak and everything is exposed.
# Looks harmless enough, but...
DB_PASSWORD=supersecret123
SMTP_PASSWORD=alsosecret
STRIPE_SECRET_KEY=sk_live_thisshouldnotleak
Docker Secrets: The Built-in Solution
Docker has a secrets mechanism. It works out-of-the-box in Swarm mode, but even without Swarm there's a file-based approach that gets the job done.
services:
api:
image: myapp:latest
secrets:
- db_password
- smtp_password
environment:
DB_PASSWORD_FILE: /run/secrets/db_password
secrets:
db_password:
file: ./secrets/db_password.txt
smtp_password:
file: ./secrets/smtp_password.txt
The difference? Secrets get mounted as files in /run/secrets/, not as environment variables. That's safer. Environment variables show up in docker inspect, in logs, in crash dumps. Files in /run/secrets/ don't.
The application needs to support it though. Many frameworks understand the _FILE suffix convention — Laravel, Node.js libraries, the official Postgres image. The app reads the path from DB_PASSWORD_FILE and fetches the value from there.
Separating Multiple Environments
A compose setup that serves both staging and production needs clear separation. Override files handle this well.
# Base configuration
docker-compose.yml
# Environment-specific overrides
docker-compose.staging.yml
docker-compose.production.yml
The base file contains everything shared: services, networks, volumes. Override files add environment-specific details — different images, resource limits, secrets.
# docker-compose.production.yml
services:
api:
image: myapp:v2.3.1 # Pinned version, no :latest
deploy:
resources:
limits:
memory: 512M
secrets:
- prod_db_password
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
secrets:
prod_db_password:
file: /etc/myapp/secrets/db_password
Starting it up:
docker compose -f docker-compose.yml -f docker-compose.production.yml up -d
The beauty here: production secrets live in a completely different location than staging secrets. No mix-ups possible.
Hooking Up External Secret Managers
For serious setups, storing secrets as files on the server isn't ideal. Better to pull them from a vault. HashiCorp Vault, AWS Secrets Manager, Azure Key Vault — they all do the same thing: centralized secret storage with on-demand access.
A pragmatic approach that doesn't require rearchitecting everything:
#!/bin/bash
# pull-secrets.sh - runs before docker compose up
set -euo pipefail
mkdir -p /tmp/app-secrets
# Pull secrets from vault
vault kv get -field=password secret/myapp/db > /tmp/app-secrets/db_password
vault kv get -field=key secret/myapp/stripe > /tmp/app-secrets/stripe_key
# Lock down permissions
chmod 600 /tmp/app-secrets/*
# Start the stack
docker compose up -d
# Clean up (secrets are now only inside containers)
rm -rf /tmp/app-secrets
Not the most elegant solution ever, but it works. Secrets exist briefly on disk, get loaded into containers, then get cleaned up. Better than sitting permanently in a .env file.
Rotation Without Downtime
Passwords change. API keys expire. Secrets need rotation without bringing everything down.
With file-based secrets, it's relatively simple:
- Update the secret file
- Restart only the service that needs it:
docker compose restart api - The container picks up the new file on start
With environment variable-based secrets, a full recreate is needed:
docker compose up -d --force-recreate api
More disruptive. Yet another reason to prefer file-based secrets over environment variables.
Practical Checklist
A few things that help keep secrets under control:
- Scan your repo with tools like
gitleaksortrufflehog. Automated, in CI. Not manually. - Use
.env.examplein the repo with dummy values. New team members know which variables are needed without seeing the real values. - Set file permissions properly. Secret files:
chmod 600. Owner-only read access. - Rotate regularly. Set a reminder. Every 90 days minimum for database passwords.
- Don't log secrets. Sounds obvious, but check your application logging. A debug statement that dumps the entire environment is written faster than you'd think.
Wrapping Up
Secrets management doesn't have to be complex. Start by separating configuration from secrets, use file-based secrets where possible, and build from there. Perfect is the enemy of good enough — a .env file that's not in git is already a massive improvement over hardcoded credentials in the source code. But there's more to gain, and the step toward Docker secrets or an external vault is smaller than it seems.
