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.

Docker ComposeSecretsOperations basics
What you learn

How Compose reads environment variables, where to use .env, when to use mounted secret files, and how to avoid leaking credentials into Git.

Best for

Self-hosted apps, VPS deployments, internal tools, and anyone tired of copying random Compose examples with plaintext passwords inside them.

Risk to watch

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.

Warning: A private repository is not the same thing as a safe secret storage system. Secrets copied into Git tend to spread into backups, forks, screenshots, logs, and chat messages.
Important scope note: This guide shows a simple single-host pattern using .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 .env file.
  • Container environment variables, where values are passed into the running container under environment: or env_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-app

Render what Compose sees:

docker compose config

This 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:

  • .env for machine-local or deployment-local values like ports, domain names, image tags, and non-sensitive defaults.
  • .env.example for 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
*.key

Create a safe .env.example:

APP_PORT=8080
APP_ENV=production
APP_NAME=myapp
DOMAIN=app.example.com
DB_USER=myapp
DB_NAME=myapp

The 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.txt

Use 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 config

Then start the app:

docker compose up -d

Check logs without printing secrets unnecessarily:

docker compose ps
docker compose logs --tail=50

When you need to rotate a secret:

  1. Generate a new value in the relevant secret file.
  2. Restart only the services that depend on it.
  3. Verify the app reconnects or authenticates correctly.
  4. 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.

Practical rule: Treat 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.