20.2.1.4 Compose Env Files
A focused guide to Compose Env Files, connecting core concepts with practical Docker and container operations.
Environment files in Docker Compose provide a clean way to manage configuration values — database passwords, API keys, port numbers, feature flags — without hardcoding them in compose.yml or in application source code. Compose supports two distinct mechanisms for environment files: the project-level .env file that Compose reads automatically for variable substitution, and the per-service env_file key that passes variables directly into a container's environment. Understanding both and how they differ prevents configuration errors.
The Project-Level .env File
Compose automatically reads a file named .env in the same directory as compose.yml. Values defined in .env are available for variable substitution inside compose.yml using ${VARIABLE_NAME} syntax:
.env:
POSTGRES_PASSWORD=devsecret
APP_PORT=3000
NODE_ENV=development
compose.yml:
services:
api:
build: .
ports:
- "${APP_PORT}:3000"
environment:
NODE_ENV: ${NODE_ENV}
db:
image: postgres:15-alpine
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
When Compose reads compose.yml, it substitutes ${APP_PORT}, ${NODE_ENV}, and ${POSTGRES_PASSWORD} with the values from .env. The substitution happens in the Compose file itself — the resulting configuration is what Compose uses to create and configure containers.
What .env Controls
The .env file controls Compose file configuration — service names, image tags, port mappings, volume names, and any other value that can appear in compose.yml. It does not automatically inject variables into the container's environment. A variable defined in .env is only available inside a container if it is explicitly referenced in the service's environment section.
This is a common source of confusion: defining NODE_ENV=production in .env does not make NODE_ENV available inside the container unless compose.yml includes:
environment:
NODE_ENV: ${NODE_ENV}
The env_file Service Key
The env_file key specifies one or more files whose contents are loaded directly as environment variables into the container, without needing to list each variable in the environment section:
compose.yml:
services:
api:
build: .
env_file:
- .env.api
.env.api:
DATABASE_URL=postgres://user:pass@db:5432/myapp
REDIS_URL=redis://cache:6379
SECRET_KEY=abc123xyz
NODE_ENV=production
LOG_LEVEL=info
Every variable in .env.api becomes available inside the api container as an environment variable. This is different from the project-level .env — env_file passes variables into the container; .env substitutes values in the Compose file itself.
Multiple env_file Entries
A service can load multiple env files, applied in order (later files override earlier ones for the same key):
services:
api:
env_file:
- .env.common
- .env.local
.env.common might define shared defaults; .env.local might override specific values for the local development environment.
Combining .env and env_file
Both mechanisms can be used together:
compose.yml:
services:
api:
build: .
ports:
- "${APP_PORT}:3000"
env_file:
- .env.api
environment:
NODE_ENV: ${NODE_ENV}
Here, APP_PORT and NODE_ENV are read from .env for Compose-level substitution. The variables in .env.api are passed directly into the container. The explicit environment entry for NODE_ENV overrides any NODE_ENV that might appear in .env.api.
Precedence Order
When the same variable is defined in multiple places, Compose applies this precedence (highest wins):
- Variables set in the
environmentsection ofcompose.yml. - Variables from
env_filefiles (later files in the list override earlier ones). - Variables from the project-level
.envfile (for Compose variable substitution only, not direct container injection unless referenced inenvironment).
Protecting Secrets with .gitignore
The .env and env files typically contain passwords, API keys, and other secrets. These must never be committed to version control:
.gitignore:
.env
.env.*
!.env.example
The negation !.env.example allows committing a template file with placeholder values that other developers can copy and fill in:
.env.example:
POSTGRES_PASSWORD=changeme
APP_PORT=3000
NODE_ENV=development
SECRET_KEY=your_secret_key_here
This pattern — commit .env.example, ignore .env — is the standard approach for Compose projects that need secrets.
Verifying What Variables a Container Receives
To inspect all environment variables set inside a running container:
docker compose exec api env
Or for a specific variable:
docker compose exec api printenv NODE_ENV
This shows the actual environment the running process sees, regardless of how the variables were injected (.env, env_file, environment, or inherited from the host shell).
Using Shell Environment Variables
If a variable is referenced in compose.yml but not defined in .env, Compose falls back to the shell environment where docker compose is running:
export APP_PORT=8080
docker compose up -d
If APP_PORT is not in .env, Compose uses the shell's APP_PORT=8080. This allows CI/CD systems to inject configuration through environment variables without needing an .env file in the repository.
Variable Substitution Syntax
Compose supports several substitution forms:
environment:
# Simple substitution
PORT: ${APP_PORT}
# Default value if variable is unset or empty
PORT: ${APP_PORT:-3000}
# Default value only if variable is unset (not empty)
PORT: ${APP_PORT-3000}
# Error if variable is unset or empty
PORT: ${APP_PORT:?APP_PORT is required}
The default-value forms (:- and -) prevent Compose from failing with an "undefined variable" error when .env is not present — useful for optional configuration with sensible defaults.