Loading .env Files in Docker and Compose
A .env file is a local-development convenience, but the moment a Python service is containerized the question changes: does the file travel into the image, get injected as environment variables, or stay on the host? Getting this wrong either leaks secrets into image layers or leaves the container with no configuration at all. This page shows the correct runtime-injection pattern, and how it relates to the .env file management rules and the precedence order.
Problem 1: baking .env into the image
Copying the file into the image is the most common and most dangerous mistake.
# Dockerfile — ANTI-PATTERN
COPY .env /app/.env # secrets are now in an image layer, forever
Anyone who can pull the image can run docker history and extract the layer. The secret is also immutable — rotating it means rebuilding and redeploying the image.
Problem 2: expecting python-dotenv to run in production
The mirror-image mistake is assuming load_dotenv() does the work everywhere.
# app.py — ANTI-PATTERN in a container
from dotenv import load_dotenv
load_dotenv() # there is no .env in the container; this loads nothing
In a container the platform injects real environment variables; there is no file to load. Relying on load_dotenv() silently gives you an unconfigured process.
Secure implementation
Inject at runtime with Compose’s env_file, and read os.environ (or a settings model). Keep .env on the host and gitignored.
# config.py — reads injected env vars; .env is a local-only fallback
from pathlib import Path
from dotenv import load_dotenv
from pydantic_settings import BaseSettings, SettingsConfigDict
# In local dev only: load .env WITHOUT overriding real env vars.
load_dotenv(Path(__file__).parent / ".env", override=False)
class Settings(BaseSettings):
model_config = SettingsConfigDict(extra="forbid")
database_url: str
redis_url: str = "redis://localhost:6379/0"
settings = Settings() # validated from injected env vars in the container
# compose.yaml — inject the host .env as container environment variables
services:
app:
build: .
env_file:
- .env # read on the host, injected as env vars; not copied in
The container process reads validated environment variables; the .env file never enters the image.
Gotchas & version-specific behaviour
- Compose
env_filedoes not expand shell variables inside the file — values are literal. - Values set with
environment:in Compose override those fromenv_file, matching the precedence order. docker run --env-file .envbehaves like Compose’senv_file;-e KEY=valuetakes precedence over it.override=Falsematters here: if a stray.envexists in the image build context, it must never win over the injected variable.- For real secrets, prefer Docker/Swarm secrets or a manager over
env_file; env vars are visible to anyone who candocker inspect.
Production parity checklist
- Gitignore
.envand ship a.env.examplewith dummy values. - Never
COPY .envin the Dockerfile; scan image layers for secrets in CI. - Use
env_file(dev) and a secret manager (prod) so the same code readsos.environin both. - Load
.envwithoverride=Falseso injected values always win. - Validate every injected variable through a
BaseSettingsmodel at startup.
Frequently asked questions
Does Docker Compose’s env_file put values in the container environment or in a file?
env_file reads the listed file on the host and injects each key as a real environment variable into the container process. The file itself is not copied into the container, so your Python code reads os.environ, not the file.
Should I COPY a .env file into my Docker image?
No. Anything COPYed into an image is readable in the image layers forever, including by anyone who pulls the image. Inject values at runtime with Compose env_file, --env-file, or a secret manager instead.
Why does my python-dotenv call do nothing inside the container?
If the platform already injected the variables, there is no .env file in the container to load, and load_dotenv finds nothing. That is correct — read os.environ, and treat .env as a local-dev-only fallback with override=False.
Key takeaways
Inject configuration into containers at runtime and read it through a validated settings model; never bake a .env into an image. The .env file is a local-dev fallback loaded with override=False, nothing more.