Keeping Secrets Out of Git: A Hard Lesson with Docker Compose
We've all been there. You're wiring up a new Docker stack, things are finally working, and you commit and push before realizing your password is sitting right there in plain text in your compose.yaml. In my case it was MLB credentials for mlbserver. Oops.
Here's how I cleaned it up and what I'm doing going forward.
What Happened
I committed a compose.yaml with credentials hardcoded directly in the environment block:
environment:
- account_username=zackwag@gmail.com
- account_password=hunter2
Pushed it to a public GitHub repo. Caught it quickly, reset the password, but the damage was done — the secret was in the git history even after I deleted the file.
Removing It From History
My first instinct was BFG Repo Cleaner, but BFG matches on filename only — not path. Since I have multiple compose.yaml files across my stacks, that was a non-starter.
git filter-repo supports path filtering, which is exactly what I needed:
cd /tmp
git clone https://github.com/zackwag/docker.git
cd docker
git filter-repo --path opt/stacks/channels-addons/compose.yaml --invert-paths
git remote add origin https://github.com/zackwag/docker.git
git push --force origin main
Worth noting: git filter-repo refuses to run on a non-fresh clone by default. Clone fresh, run it there, force push. Don't fight it.
The Right Pattern Going Forward
The fix is straightforward — .env files. Keep secrets out of the compose file entirely and reference them as variables.
compose.yaml
environment:
- account_username=${MLB_USERNAME}
- account_password=${MLB_PASSWORD}
.env (never committed)
MLB_USERNAME=zackwag@gmail.com
MLB_PASSWORD=your_password_here
.env.example (committed as a template)
MLB_USERNAME=
MLB_PASSWORD=
.gitignore
.env
Docker Compose picks up .env automatically from the same directory as your compose.yaml. No extra configuration needed.
Not Everything Needs to Be a Secret
Worth calling out — not everything in your compose file needs to move to .env. In my Caddy stack I have things like DOMAIN, EMAIL, upstream IPs, and internal TLDs. None of that is sensitive. The rule of thumb:
- Secrets →
.env(passwords, tokens, API keys) - Config → fine in
compose.yaml(domains, IPs, emails, paths)
Bonus: Nuking Your Git History Entirely
Since I'd already made a mess of the history, I took the opportunity to squash everything down to a single clean commit:
git checkout --orphan fresh
git add -A
git commit -m "Initial commit"
git branch -D main
git branch -m main
git push --force origin main
Clean slate. Felt good.
Takeaways
- Add
.gitignoreand.env.examplebefore you write your firstcompose.yaml - If you do commit a secret, reset it immediately — history cleanup is hygiene, not the fix
git filter-repois the right tool for surgical history rewrites- Public repo bots are fast. Assume any exposed secret was seen.