There are three distinct mechanisms for environment variables in Docker and Compose. They are easy to conflate but serve different purposes.
1. .env — Variable Substitution in the Compose File Itself
A .env file in the same directory as docker-compose.yml is read by Compose before the file is parsed. Its values fill in ${VARIABLE} placeholders in the YAML.
# .env
POSTGRES_VERSION=15
DJANGO_PORT=8000
IMAGE_TAG=abc123# docker-compose.yml
services:
db:
image: postgres:${POSTGRES_VERSION} # becomes postgres:15
web:
image: mydjango:${IMAGE_TAG} # becomes mydjango:abc123
ports:
- "${DJANGO_PORT}:8000" # becomes 8000:8000Important: .env substitutes values into the Compose file structure. It does not automatically inject anything into the container’s environment. It is a templating tool for the YAML, not a runtime secrets mechanism.
.env belongs in .gitignore. It is a local override file, not something to commit.
2. environment: — Variables Injected into the Container at Runtime
The environment: key sets environment variables inside the running container. This is what Django reads via os.environ.
services:
web:
environment:
# Hardcoded value — always set to this
DJANGO_SETTINGS_MODULE: myproject.settings.production
# Pulled from the host shell's environment or .env file at Compose-parse time
SECRET_KEY: ${SECRET_KEY}
# No value — passes through whatever the host shell has set
# If the host doesn't have it, the variable is unset in the container
SENTRY_DSN:| Form | Source | What happens if missing |
|---|---|---|
KEY: value | Hardcoded in file | Always set |
KEY: ${VAR} | Host shell or .env | Empty string or warning |
KEY: (no value) | Host shell only | Unset in container |
In Django settings.py:
import os
SECRET_KEY = os.environ["SECRET_KEY"] # raises KeyError if missing — intentional
DEBUG = os.environ.get("DEBUG", "false") == "true"
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "localhost").split(",")3. env_file: — Load Variables from a File into the Container
env_file: reads a file of KEY=VALUE pairs and injects them directly into the container’s environment. Unlike .env, this file is not used for YAML substitution — it goes straight into the container.
services:
web:
env_file:
- .env.local # loaded in all environments
- .env.production # stacked — later files win on conflicts# .env.production
DJANGO_SETTINGS_MODULE=myproject.settings.production
SECRET_KEY=your-secret-key-here
DATABASE_URL=postgres://user:password@db:5432/myproject
REDIS_URL=redis://cache:6379
ALLOWED_HOSTS=myapp.com,www.myapp.comWhen Injection Actually Happens
This is the critical point: environment variables are injected at container start, not at image build time.
docker compose up
↓
Compose reads docker-compose.yml (substituting .env values)
↓
Compose calls docker run with -e KEY=VALUE flags
↓
Kernel creates the process with those variables in its environment
↓
Django reads them via os.environ at settings import time
The image contains none of these values. The same image can run with DJANGO_SETTINGS_MODULE=myproject.settings.development locally and myproject.settings.production in prod. The image is environment-agnostic; configuration is injected at runtime.
What this means for secrets: Never bake secrets into an image via ENV in a Dockerfile. They will be visible in docker inspect, image history, and any registry you push to. Always inject at runtime via environment: or env_file:.
The Security Hierarchy
From least to most secure:
Hardcoded in docker-compose.yml ← worst: committed to git
Committed .env file ← bad: in version control
env_file: pointing to gitignored file ← acceptable for local dev
environment: from host shell ← better: set by CI/CD or operator
AWS Secrets Manager / Vault / SSM ← best: never touches disk as plaintext
For local development, a gitignored .env or .env.local file is the pragmatic choice. For production, secrets should come from a secrets manager and be injected by your CI/CD pipeline — never stored in files on the server.
Summary — Which Mechanism Does What
| Mechanism | Scope | Used for |
|---|---|---|
.env file | Compose YAML only | Templating compose file values (image tags, port numbers) |
environment: | Container runtime | Injecting config and secrets into the running process |
env_file: | Container runtime | Same as environment: but loaded from a file |