How to Manage Env Files and Secret Files in Docker Compose
Keep configuration flexible and credentials safer by using env files, bind-mounted secret files, and cleaner Docker Compose habits instead of hardcoding sensitive values.
How Compose reads environment variables, where to use .env, when to use mounted secret files, and how to avoid leaking credentials into Git.
Self-hosted apps, VPS deployments, internal tools, and anyone tired of copying random Compose examples with plaintext passwords inside them.
Many secret leaks happen because config was convenient in the moment and never cleaned up before a repo was pushed or shared.
Before you begin
- Docker and Docker Compose installed on your machine or VPS.
- A Compose app directory you control, such as
~/apps/myapp. - Basic comfort with editing text files over SSH.
- A willingness to stop storing passwords directly inside
compose.yml.
Configuration is one of the first places small deployments become messy. A project starts with one container, then grows into a database password, SMTP credentials, API tokens, and domain names. If all of that lives inline inside the Compose file, the setup becomes harder to audit, harder to share safely, and easier to leak by accident.
Why this matters more than it looks
Environment variables are not magic security. They are just one way to pass configuration. Their real value is operational clarity. They let you change values between environments without rewriting your app definition. For truly sensitive values, a dedicated secret file or external secret manager is often better than pasting raw credentials into YAML.
.env files plus bind-mounted secret files. It is not a guide to Docker Swarm or Compose-managed secrets: objects, which behave differently and are not available in the same way in every small Compose deployment.Step 1: Understand how Compose reads env data
Compose commonly uses env data in two different ways:
- Variable substitution in the Compose file, where values like
${APP_PORT}are filled from your shell environment or a local.envfile. - Container environment variables, where values are passed into the running container under
environment:orenv_file:.
Those two ideas are related, but not identical. Beginners often confuse them and then wonder why a variable is visible in one place but not another.
Create a small example in compose.yml:
services:
app:
image: nginx:alpine
ports:
- "${APP_PORT}:80"
environment:
APP_ENV: ${APP_ENV}
APP_NAME: ${APP_NAME}Then create a .env file in the same directory:
APP_PORT=8080
APP_ENV=production
APP_NAME=demo-appRender what Compose sees:
docker compose configThis command is one of the best sanity checks you can use. It shows the fully resolved configuration before you start containers.
Step 2: Build a cleaner project layout
A practical beginner-friendly structure looks like this:
myapp/
├── compose.yml
├── .env
├── .env.example
├── secrets/
│ ├── db_password.txt
│ └── smtp_password.txt
└── data/Use each file for a specific purpose:
.envfor machine-local or deployment-local values like ports, domain names, image tags, and non-sensitive defaults..env.examplefor a shareable template that documents required variables without exposing real credentials.secrets/for sensitive values stored as protected local files that should not be committed.
Add ignore rules to Git:
.env
secrets/
*.pem
*.keyCreate a safe .env.example:
APP_PORT=8080
APP_ENV=production
APP_NAME=myapp
DOMAIN=app.example.com
DB_USER=myapp
DB_NAME=myappThe goal is not perfection. The goal is to make it obvious which values are safe to share and which values are private.
Step 3: Use bind-mounted secret files for sensitive values
Some applications can read passwords from files, which is usually nicer than embedding them directly in environment variables. A common pattern is bind-mounting a local file read-only into the container, then pointing the app at that file. Support for variables like POSTGRES_PASSWORD_FILE is image-specific, so always confirm the exact image documentation before copying the pattern.
Create a password file with restrictive permissions:
mkdir -p secrets
openssl rand -base64 32 > secrets/db_password.txt
chmod 600 secrets/db_password.txtUse it in Compose:
services:
db:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
volumes:
- db-data:/var/lib/postgresql/data
- ./secrets/db_password.txt:/run/secrets/db_password:ro
volumes:
db-data:If your image does not support a _FILE convention, read its documentation. Some apps need a direct environment variable, while others support config files instead. Docker Compose itself does not magically translate arbitrary variables into file-backed secrets for you; the container image or application has to support that pattern. When forced to use environment variables, keep them out of version control and avoid echoing them into shell history or logs.
For small single-host deployments, mounted read-only secret files are often a very reasonable compromise. For larger teams or many environments, move toward a dedicated secret manager later.
Step 4: Verify, rotate, and operate safely
Before bringing the stack up, validate the resolved configuration:
docker compose configThen start the app:
docker compose up -dCheck logs without printing secrets unnecessarily:
docker compose ps
docker compose logs --tail=50When you need to rotate a secret:
- Generate a new value in the relevant secret file.
- Restart only the services that depend on it.
- Verify the app reconnects or authenticates correctly.
- Remove any old copies left in temporary files or notes.
Expected outcome: You can change ports, domains, and non-sensitive settings without editing YAML logic, and your real passwords no longer live in committed Compose files.
Troubleshooting common mistakes
Compose says a variable is not set.
Make sure the .env file is in the same directory where you run docker compose, or export the variable in your shell first.
The container started, but the app still cannot read its password.
Check whether the image expects a raw variable like DB_PASSWORD or a file-based variable like DB_PASSWORD_FILE. They are not interchangeable unless the application explicitly supports both.
I accidentally committed a secret.
Rotate it immediately. Then remove it from the repository history if needed, but do not assume deletion alone makes the credential safe again.
Permissions errors on secret files.
Verify the file exists, the mount path is correct, and the container user can read the mounted file. Using :ro on the mount is a good default.
docker compose config as your preflight check. It catches missing substitutions early and saves time before a broken deploy.What to do next
Once your config is cleaner, the next step is improving how updates move from your machine to a VPS. Continue with How to Use rsync for Fast, Safe VPS Deployments.
