How does Compose handle environment variables and .env files?
Quick Answer
Compose automatically reads a file named .env in the same directory as the compose file, making its variables available for substitution (${VAR_NAME}) anywhere in the compose file itself. Separately, the environment: key sets environment variables inside a service's container at runtime, and env_file: loads an entire file's worth of variables into a service's container in one step — these are two related but distinct mechanisms: .env substitutes values into the compose file's own YAML, while environment/env_file set what the running container actually sees.
Detailed Answer
Two distinct mechanisms that are easy to conflate
1. The .env file — substituted into the compose file itself, at parse time
# .env (same directory as compose.yaml)
DB_PASSWORD=supersecret
API_PORT=3000
# compose.yaml
services:
api:
ports:
- "${API_PORT}:3000"
environment:
- DB_PASSWORD=${DB_PASSWORD}
Compose automatically reads a file literally named .env in the project directory. It uses its values to fill in any ${VAR_NAME} placeholders within the compose file's own YAML text, before the file is even processed. This happens regardless of whether those placeholders end up inside an environment: block, a ports: mapping, an image tag, or anywhere else in the file.
2. environment: / env_file: — set inside the running container
services:
api:
environment:
- NODE_ENV=production # set directly, a literal value
- DB_PASSWORD=${DB_PASSWORD} # set via .env substitution (mechanism #1, feeding into #2)
env_file:
- .env.api # load an ENTIRE separate file's variables into the container
environment: and env_file: control what environment variables the running container itself actually sees, equivalent to docker run -e. This is a runtime concern for the application inside the container, distinct from the .env file's job of substituting values into the compose YAML's own text at parse time.
Why the distinction matters in practice
services:
api:
image: myapi:${APP_VERSION} # .env substitution determines WHICH IMAGE TAG to use
environment:
- APP_VERSION=${APP_VERSION} # separately, ALSO expose that same value to the running app
A value from .env can be used purely to parameterize the compose file itself, like choosing an image tag or a host port, without necessarily also being passed into the container's environment. Conversely, a container's environment: block can set variables that have nothing to do with any .env substitution at all, using plain literal values. Confusing "this variable is available for compose-file templating" with "this variable is available inside my running application" is a common source of "why isn't my app seeing this environment variable" confusion.
A dedicated env_file, separate from .env
# .env.api
NODE_ENV=production
LOG_LEVEL=info
DB_CONNECTION_STRING=postgres://...
services:
api:
env_file:
- .env.api
env_file: is specifically for loading a whole file's worth of variables directly into one service's container. This is distinct from the special, automatically-loaded .env file, which is scoped to the whole compose file's variable substitution, not tied to any one particular service.
Never commit real secrets into either mechanism, uncommented
# .gitignore
.env
.env.*
Both .env and any custom env_file are plain, unencrypted text. Genuinely sensitive values, such as database passwords or API keys, should not be committed to version control in either form. The standard practice is to .gitignore them and provide a .env.example template with placeholder, non-sensitive values showing the expected shape. This mirrors the same secrets-hygiene principle covered in the SQL/Databases and Kubernetes stacks.