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:

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