π 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
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
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}
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
Provide a safe template for your team:
# .env.example β commit this one
POSTGRES_USER=
POSTGRES_PASSWORD=
POSTGRES_DB=
β
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 initonce.
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
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
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
β
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
Step 2 β Add secrets directory to .gitignore:
echo "secrets/" >> .gitignore
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
β
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
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
Make it executable:
chmod +x .git/hooks/pre-commit
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 - [ ]
.envis in.gitignore - [ ]
.env.exampleexists 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)