Skip to main content

How to Keep Passwords Out of compose.yaml with an .env File

· 8 min read
Jagdish Kumawat
Founder @ Dewiride

A password typed into compose.yaml ends up in Git, and then in every copy of the repository. Docker Compose can read passwords from a separate .env file instead, and with one small piece of syntax it refuses to start when they're missing. This guide sets that up for a MySQL container, with the real output of every check.

How Compose Reads the .env File​

When you run a docker compose command, Compose looks for a file named .env in the project folder, the folder that holds compose.yaml. Every ${NAME} in compose.yaml is replaced with the value of NAME from that file. Docker calls this interpolation.

Docker's documentation lists a stricter form, which this guide uses for every password:

${VAR:?error} -> value of VAR if set and non-empty, otherwise exit with error

So a missing or empty password stops Compose with your own message, instead of starting a database with no password.

The project here is the local MySQL database from How to Run MySQL in Docker with Docker Compose. It uses three files:

FileContainsIn Git?
compose.yamlThe container set-up, with ${…} where passwords goYes
.envThe real passwordsNo, it's ignored
.env.exampleThe same setting names with empty valuesYes

Prerequisites​

  • Docker Desktop (or Docker Engine with the Compose plugin). Check it:
Terminal
docker compose version
Output
Docker Compose version v5.6.1

Step 1: Reference the Passwords in compose.yaml​

In compose.yaml, write each secret as a variable with a helpful error message after :?:

compose.yaml (the environment section)
environment:
MYSQL_DATABASE: inventory
MYSQL_USER: inventory_app
# The passwords come from the .env file next to this one.
MYSQL_PASSWORD: ${MYSQL_PASSWORD:?Set MYSQL_PASSWORD in the .env file}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:?Set MYSQL_ROOT_PASSWORD in the .env file}

The database name and user name aren't secret, so they stay as plain values. Only the two passwords move out.

Step 2: Create .env.example​

.env.example tells anyone who clones the project which settings to provide, without giving away any values. Create it next to compose.yaml:

.env.example
# Passwords for the local MySQL container that compose.yaml starts.
# Copy this file to .env and give each setting a strong random password.
# .env is ignored by Git, so your real passwords never reach the repository.

# Password of MySQL's built-in administrator account, root.
MYSQL_ROOT_PASSWORD=

# Password of the inventory_app account that the API connects with.
MYSQL_PASSWORD=

The .env.example file in a macOS-style code window, with comments explaining that it should be copied to .env, and empty MYSQL_ROOT_PASSWORD and MYSQL_PASSWORD settings.

The values are left empty on purpose. If someone copies the file without filling it in, the :? rule stops Compose with a clear message.

Step 3: Create .env with Real Passwords​

This command creates .env with two strong random passwords, and they never appear on screen. How to Generate a Strong Random Password in the Terminal explains how it works.

Terminal
printf "MYSQL_ROOT_PASSWORD=%s\nMYSQL_PASSWORD=%s\n" "$(openssl rand -hex 24)" "$(openssl rand -hex 24)" > .env

The command prints nothing. Your .env now has two lines, each a setting name followed by 48 random hex characters.

Windows line endings are fine

PowerShell on Windows ends lines with CR LF instead of LF. I tested a .env file with CR LF line endings: Compose read the values without the extra character.

Step 4: Check Compose Can Read Them, Without Printing Them​

Terminal
docker compose config --quiet

docker compose config reads compose.yaml and .env and checks them, and --quiet only reports problems. No output means everything is valid. The command works unchanged in bash, zsh and PowerShell.

Plain docker compose config prints your passwords

Without --quiet, docker compose config prints the whole file with every ${…} replaced by its real value, passwords included. Never run it while sharing your screen or recording a video, and never paste its output into a ticket or chat.

Step 5: See What Happens Without .env​

This is what a teammate sees if they clone the project and forget to create .env. I ran it in a copy of the folder without the file:

Terminal
docker compose config --quiet
Output
error while interpolating services.mysql.environment.MYSQL_PASSWORD: required variable MYSQL_PASSWORD is missing a value: Set MYSQL_PASSWORD in the .env file
error while interpolating services.mysql.environment.MYSQL_ROOT_PASSWORD: required variable MYSQL_ROOT_PASSWORD is missing a value: Set MYSQL_ROOT_PASSWORD in the .env file

A terminal where docker compose config --quiet fails without an .env file: required variable MYSQL_PASSWORD is missing a value, and the same for MYSQL_ROOT_PASSWORD, each followed by the custom message Set … in the .env file.

Compose names both missing settings and adds your message, and nothing starts. docker compose up stops with the same errors.

Step 6: Confirm Git Ignores .env​

Terminal
git check-ignore -v .env
Output
.gitignore:7:.env .env

Line 7 of .gitignore ignores .env, so git add . will never pick it up. .env.example isn't ignored, and you commit it with compose.yaml.

Common Mistakes​

MistakeWhat happensDo this instead
Writing the password directly in compose.yamlIt's committed to Git and shared with every cloneUse ${NAME:?message} and put the value in .env
Using ${NAME} without :?Compose only warns The "MYSQL_PASSWORD" variable is not set. Defaulting to a blank string. and carries on with an empty valueAlways use :? for secrets
Putting .env in a different folderCompose doesn't find it and reports the variables as missingKeep .env next to compose.yaml
Changing a MySQL password in .env after the database existsMySQL keeps the old one, and sign-in failsChange it inside MySQL too, see Fixed: ERROR 1045 (28000): Access denied for user

Troubleshooting​

SymptomLikely causeFix
required variable … is missing a value.env is missing, empty, or the name is misspelledCheck the file is next to compose.yaml, and the names match exactly
.env shows up in git statusIt isn't in .gitignoreAdd a line .env to .gitignore
A value with $ in it gets cut off (ab$cdef becomes ab)Compose reads $ in .env as the start of another variableUse hex passwords, which never contain $, or wrap the value in single quotes

FAQ​

Is .env secure enough for production?​

For a development database on your own computer, yes. On servers, prefer a secret store your platform provides. The official MySQL image also accepts MYSQL_PASSWORD_FILE and MYSQL_ROOT_PASSWORD_FILE, which read the password from a file such as a Docker secret instead of an environment variable.

Do I commit .env.example?​

Yes. It contains only setting names and comments, and it tells everyone else which values they need.

Does docker compose up read .env automatically?​

Yes. Every docker compose command reads .env from the project folder. You don't need any extra flag.

Conclusion​

Passwords now live only in .env, which Git ignores. compose.yaml refers to them with ${NAME:?message}, so Compose stops with a clear error when one is missing, and .env.example documents what to fill in. docker compose config --quiet checks the set-up without printing a single secret.

Additional Resources​

Stay Updated

Subscribe to our newsletter for the latest tutorials, tech insights, and developer news.

By subscribing, you agree to our privacy policy. Unsubscribe at any time.