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

March 2, 20264 min. read

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:

  1. Update the secret file
  2. Restart only the service that needs it: docker compose restart api
  3. 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 gitleaks or trufflehog. Automated, in CI. Not manually.
  • Use .env.example in 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.

Want to stay updated?

Subscribe to my newsletter or get in touch for freelance projects.

Get in Touch