How to Keep Passwords Out of Docker Compose Files (.env Files and Secrets Done Right)

Every compose file in this series had a line like MARIADB_PASSWORD: changeme. That's fine for a tutorial; it's not fine for a folder you back up, paste into a forum when asking for help, or push to Gitea. The fix is small: passwords go in a .env file next to the compose file, the compose file refers to them by name, and the .env file is locked down and excluded from anything shared.
Quick reference
| Task | What |
|---|---|
| Secrets file | .env in the same folder as compose.yaml |
| Refer to one | ${DB_PASSWORD} in compose.yaml |
| Generate a strong one | openssl rand -base64 32 |
| Lock it down | chmod 600 .env |
| See the result | docker compose config |
| Keep out of git | .env in .gitignore |
1. Move the secrets
In ~/docker/nextcloud:
nano .env
DB_ROOT_PASSWORD=Gk3qvLw9ZtPq2m8Xn4Rb7Yc1Vd5Hs0Jf
DB_PASSWORD=Wq8Lm2Nx5Pv1Zr7Tb4Yc9Hd3Kf6Gs0Qa
Make them with openssl rand -base64 32 — a random string you never have to remember, because nothing but these two containers ever uses it.
chmod 600 .env
2. Refer to them in compose.yaml
services:
db:
image: mariadb:11
environment:
MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MARIADB_PASSWORD: ${DB_PASSWORD}
app:
environment:
MYSQL_PASSWORD: ${DB_PASSWORD}
Compose reads .env automatically from the folder where you run it. docker compose up -d and nothing else changes — the containers see the real values.
3. Check what the container will get
docker compose config
prints the compose file with every ${...} filled in. If a value is blank, the variable name doesn't match the .env file.
Don't commit the output of that command anywhere either.
4. Changing a password later
The database stored the password at first start; changing .env alone won't change it. For MariaDB: docker compose exec db mariadb -uroot -p → ALTER USER 'app'@'%' IDENTIFIED BY 'new';, then update .env and up -d for the app.
5. If you keep ~/docker in Gitea
Add a .gitignore at the top:
.env
**/data/
**/db/
**/postgres/
and commit a .env.example with the variable names and dummy values so you (or future you) know what to fill in.
6. Proper Docker secrets
Compose also supports file-based secrets, mounted into the container at /run/secrets/name rather than passed as environment variables (which docker inspect can reveal):
services:
db:
image: mariadb:11
environment:
MARIADB_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txt
Only images that support the _FILE convention (MariaDB, PostgreSQL, Nextcloud and many others) can use this. Where it works, it's the tidiest option; where it doesn't, .env is fine.
What else counts as a secret
API tokens for DuckDNS and Cloudflare, the Vaultwarden admin token, Telegram bot tokens, SMTP app passwords, the restic repository password. All of them belong in .env or a chmod 600 file, none in a compose file or a script you might paste.
Troubleshooting
Variable shows as empty. .env must be in the folder you run docker compose from, with KEY=value and no spaces around =, no quotes needed.
Password with $ in it breaks. Compose treats $ specially. Either escape it as $$ or generate one without it (openssl rand -hex 32 gives only letters and digits).
"permission denied" reading .env. You ran compose with sudo and the file is 600 owned by you, or vice versa. Run compose as the same user that owns the folder.
Wrapping up
Secrets in .env, chmod 600, ${NAME} in the compose file, .env in .gitignore. Ten minutes to retrofit every app, and your docker folder becomes something you can back up, share and ask for help with without leaking a thing.
