DEV Community

Cover image for Docker Compose Secrets: How to Stop Hardcoding Passwords in Your docker-compose.yml
Fedya Serafiev
Fedya Serafiev

Posted on • Originally published at itpraktika.com

Docker Compose Secrets: How to Stop Hardcoding Passwords in Your docker-compose.yml

πŸ“ This is a translated and adapted version of the original article published in Bulgarian at itpraktika.com.


We've all done it. You're spinning up a new project, you need a database, and you write something like this:

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: mysupersecretpassword123
Enter fullscreen mode Exit fullscreen mode

It works. You commit it. You push it to GitHub.

And just like that, your database password is now public. Forever. Even if you delete it later, it lives in the git history.

In this article I'll show you three proper ways to handle secrets in Docker Compose β€” from simple to production-grade β€” so you never have to hardcode credentials again.


Why This Is a Real Problem

Before we jump into solutions, let's be clear about what's at stake.

Hardcoded secrets in docker-compose.yml are dangerous because:

  • Git history never forgets. Even if you remove the password in the next commit, anyone with access to your repo history can find it.
  • It scales badly. The same file gets used in dev, staging, and production β€” with the same credentials.
  • It violates the 12-factor app principle of strict separation between config and code.
  • It's the #1 source of credential leaks in public repositories. GitHub scans for these patterns automatically and will warn you β€” but the damage is already done.

Method 1: .env Files (Quick Win for Development)

The simplest improvement. Instead of hardcoding values directly, you reference them as variables.

Step 1 β€” Create a .env file:

# .env
POSTGRES_USER=myapp
POSTGRES_PASSWORD=a-much-safer-password
POSTGRES_DB=production_db
Enter fullscreen mode Exit fullscreen mode

Step 2 β€” Reference it in docker-compose.yml:

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
Enter fullscreen mode Exit fullscreen mode

Docker Compose automatically reads .env from the same directory β€” no extra configuration needed.

Step 3 β€” The most important part. Add .env to .gitignore:

echo ".env" >> .gitignore
Enter fullscreen mode Exit fullscreen mode

Provide a safe template for your team:

# .env.example  ← commit this one
POSTGRES_USER=
POSTGRES_PASSWORD=
POSTGRES_DB=
Enter fullscreen mode Exit fullscreen mode

βœ… Pros: Simple, zero dependencies, works everywhere.
⚠️ Cons: The .env file still lives as plaintext on disk. Fine for development, not ideal for production.


Method 2: Docker Secrets (Production-Ready)

Docker has a built-in secrets management system. Secrets are stored encrypted in memory and are never written to disk or exposed as environment variables.

⚠️ Note: Docker Secrets in this form require Docker Swarm mode to be initialized. If you're running a single-node setup, you can still use it β€” just run docker swarm init once.

Step 1 β€” Create the secret:

# From a string
echo "my-super-secret-db-password" | docker secret create db_password -

# Or from a file
docker secret create db_password ./password.txt
Enter fullscreen mode Exit fullscreen mode

Step 2 β€” Reference it in docker-compose.yml:

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: myapp
      POSTGRES_DB: production_db
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password

secrets:
  db_password:
    external: true
Enter fullscreen mode Exit fullscreen mode

The secret is mounted as a file inside the container at /run/secrets/db_password. Many official Docker images (PostgreSQL, MySQL, MariaDB) support the _FILE suffix specifically for this pattern.

Step 3 β€” Deploy:

docker stack deploy -c docker-compose.yml myapp
Enter fullscreen mode Exit fullscreen mode

βœ… Pros: Secrets are encrypted at rest, never in environment variables, ideal for production.
⚠️ Cons: Requires Swarm mode. Slightly more complex to set up.


Method 3: Secrets as Local Files (Swarm-Free Alternative)

If you don't want to use Swarm mode but still want to keep secrets out of your docker-compose.yml, you can mount secret files directly.

Step 1 β€” Create a directory for secrets (outside your project if possible):

mkdir -p ./secrets
echo "my-db-password" > ./secrets/db_password.txt
echo "my-redis-password" > ./secrets/redis_password.txt
Enter fullscreen mode Exit fullscreen mode

Step 2 β€” Add secrets directory to .gitignore:

echo "secrets/" >> .gitignore
Enter fullscreen mode Exit fullscreen mode

Step 3 β€” Mount as read-only files in docker-compose.yml:

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: myapp
      POSTGRES_DB: production_db
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    volumes:
      - ./secrets/db_password.txt:/run/secrets/db_password:ro

  redis:
    image: redis:7-alpine
    command: sh -c 'redis-server --requirepass "$$(cat /run/secrets/redis_password)"'
    volumes:
      - ./secrets/redis_password.txt:/run/secrets/redis_password:ro
Enter fullscreen mode Exit fullscreen mode

βœ… Pros: No Swarm required, works with standard docker compose up, keeps secrets out of YAML.
⚠️ Cons: Secret files still live on disk β€” make sure the host machine is secure.


Bonus: Validate That You're Not Leaking Secrets

Before every commit, check that no secrets ended up in your YAML:

# Search for common patterns in your compose file
grep -Ei '(password|secret|key|token)\s*[:=]\s*[^\$\{]' docker-compose.yml
Enter fullscreen mode Exit fullscreen mode

If this command returns any output β€” you have a hardcoded secret. Fix it before committing.

You can also add this as a pre-commit hook:

# .git/hooks/pre-commit
#!/bin/sh
if grep -Ei '(password|secret|key|token)\s*[:=]\s*[^\$\{]' docker-compose.yml; then
  echo "❌ Possible hardcoded secret detected in docker-compose.yml. Aborting commit."
  exit 1
fi
Enter fullscreen mode Exit fullscreen mode

Make it executable:

chmod +x .git/hooks/pre-commit
Enter fullscreen mode Exit fullscreen mode

Which Method Should You Use?

Scenario Recommended Method
Local development .env file + .gitignore
Single-server production Local secret files (Method 3)
Multi-node / orchestrated production Docker Secrets (Method 2)
CI/CD pipelines Environment variables injected by the CI system (GitHub Actions Secrets, GitLab CI Variables, etc.)

Quick Checklist Before You Push

  • [ ] No plaintext passwords in docker-compose.yml
  • [ ] .env is in .gitignore
  • [ ] .env.example exists and is committed (with empty values)
  • [ ] secrets/ directory is in .gitignore
  • [ ] Pre-commit hook is in place

Conclusion

Hardcoding passwords in docker-compose.yml is one of those habits that's easy to fall into and hard to undo once your repo is public. The good news is that all three methods above are straightforward to implement β€” even in an existing project.

Start with .env files today if you haven't already. It takes five minutes and immediately eliminates the most common source of accidental credential leaks.

Your future self (and your security team) will thank you.


Written by Fedya Serafiev β€” itpraktika.com β€” practical IT solutions and automation.

Top comments (0)