Fix Environment Variables Not Loaded in Docker Compose
Fix Django settings missing environment variables under Docker Compose: .env versus env_file, variable precedence, recreating containers, $ in values, build-time versus runtime, and docker compose config.
Problem statement
The Django container starts with KeyError: 'DJANGO_SECRET_KEY', ImproperlyConfigured, or quietly uses development defaults, even though the variable is in a .env file next to compose.yaml. Or you changed a value, restarted, and the container still has the old one. Or a password containing $ arrives truncated.
Docker Compose has two separate mechanisms that both involve "env files", plus precedence rules and container lifecycle rules that are easy to mix up. This guide explains which file does what, how to see what a container actually received, and how to fix each common failure.
- The
.envfile in the project directory is used to substitute${VAR}insidecompose.yaml. It is not passed into containers by itself. - To pass variables into a container, list them under
environment:or reference a file withenv_file:. - Check what Compose resolved with
docker compose config, and what the container has withdocker compose exec web env. - After changing variables, run
docker compose up -dto recreate containers;docker compose restartkeeps the old environment. - Quote values containing
$with single quotes in env files, and escape$as$$insidecompose.yaml. - Variables are not available during
docker build; pass build-time values as build arguments or avoid needing them.
Step-by-step solution
1. Understand the two kinds of env files
project/
compose.yaml
.env <- read by Compose for ${VAR} substitution in compose.yaml
app.env <- passed into containers only if listed under env_file:
# compose.yaml
services:
web:
image: ghcr.io/example/myapp:${APP_TAG} # APP_TAG comes from .env or the shell
env_file:
- app.env # every line becomes a container variable
environment:
DJANGO_SETTINGS_MODULE: config.settings.production
DJANGO_ALLOWED_HOSTS: ${ALLOWED_HOSTS} # interpolated from .env or the shell
If your secrets are only in .env and nothing under environment: or env_file: references them, the container never sees them. Either move them to a file listed under env_file: or reference each one explicitly.
2. See what Compose resolved
docker compose config
This prints the final configuration after interpolation and merging, including the values from env_file. Warnings such as The "ALLOWED_HOSTS" variable is not set. Defaulting to a blank string. point directly at missing substitutions.
Then look inside the running container:
docker compose exec web env | sort | grep -E "^DJANGO_|^DATABASE_"
docker compose exec web python -c "import os; print(repr(os.environ.get('DJANGO_SECRET_KEY', 'MISSING'))[:12])"
repr() exposes stray quotes or whitespace. Avoid printing full secrets in shared terminals or logs.
3. Recreate containers after changes
A container's environment is fixed when it is created. docker compose restart web restarts the same container with the same environment. To apply new values:
docker compose up -d web
Compose detects the configuration change and recreates the container. If you changed only a file listed in env_file, up -d also picks it up; use --force-recreate if in doubt.
4. Know the precedence
When the same variable is set in several places, the container gets the value from the highest-priority source:
docker compose run -e VAR=value(one-off containers)environment:incompose.yaml(with values interpolated from the shell or.env)env_file:entriesENVin the Dockerfile
A Dockerfile ENV DJANGO_SETTINGS_MODULE=config.settings.dev is overridden by the compose file, but if the compose file does not set it, the image's development default wins silently. Avoid development defaults in production images.
For interpolation in compose.yaml, shell environment variables take precedence over the project .env file. A stale export ALLOWED_HOSTS=... in your deploy shell can override the file.
5. Handle special characters
Inside compose.yaml, $ starts an interpolation. Escape a literal dollar sign as $$:
environment:
EMAIL_HOST_PASSWORD: "abc$$def" # container receives abc$def
In env_file files, Compose (v2) applies interpolation to unquoted and double-quoted values as well. Use single quotes for values that must be taken literally:
# app.env
DJANGO_SECRET_KEY='k$8s!x#q...long-random...'
DATABASE_URL=postgres://myapp:p%40ss@db:5432/myapp
Do not use export in env files and do not put spaces around =.
6. Build time is not run time
Variables from environment: and env_file: exist only in running containers. During docker build (for example RUN python manage.py collectstatic), they do not exist, so settings that require them fail the build.
Options:
- Make build-time commands work without secrets, for example with a build settings module or harmless defaults used only during the build:
RUN DJANGO_SETTINGS_MODULE=config.settings.production \
DJANGO_SECRET_KEY=build-only \
python manage.py collectstatic --noinput
- Pass non-secret values as build arguments (
args:underbuild:in Compose,ARGin the Dockerfile). Never pass secrets as build arguments; they are visible in the image history. Use BuildKit secret mounts if a build step truly needs a secret.
7. Check the settings side
# settings/production.py
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"] # fail loudly if missing
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "").split(",")
Failing loudly with os.environ["..."] for required values is better than silent defaults. A setting with a default like os.environ.get("DEBUG", "True") turns a missing variable into a debug-mode production site.
Verification check:
docker compose config --quiet && echo "config valid"
docker compose up -d
docker compose exec web python manage.py check --deploy
docker compose exec web env | grep -c DJANGO_
Explanation
Compose processes configuration in two stages. First, it reads compose.yaml and substitutes ${VAR} using the shell environment and the project .env file. Then it creates containers whose environment comes from the resolved environment: entries, any env_file: files, and the image's ENV defaults. The project .env file only participates in the first stage, which is the source of most confusion. Because container environments are fixed at creation, configuration changes also require recreating containers.
Edge cases / notes
Different .env location
Compose looks for .env in the project directory (by default, the directory of the first compose file). Running Compose from another directory with -f, or using --env-file, changes which file is read.
Secrets in production
Env files on disk should be readable only by the deploy user (chmod 600). For stricter setups, use Docker secrets (mounted as files) or a secrets manager, and read them in settings.
When to automate this
Add docker compose config --quiet and a container start with manage.py check --deploy to CI using a dummy env file with every required key. A missing variable then fails the pipeline instead of the deploy.
Next steps
- Structure settings around the environment with Configure Django Environment Variables for Production.
- Review the whole stack in Deploy Django with Docker Compose in Production.
- If the secret key is the missing value, see Fix ImproperlyConfigured: SECRET_KEY Must Not Be Empty.
- If the container stops right after starting, see Fix a Django Docker Container That Exits Immediately.
FAQ
Does Docker Compose pass the .env file into containers?
No. The project .env file is used for variable substitution in compose.yaml. Use env_file: or environment: to pass variables into containers.
Why doesn't docker compose restart pick up my new environment variables?
restart reuses the existing container, whose environment was fixed at creation. Run docker compose up -d to recreate it.
Why is my password with a $ sign truncated in Docker Compose?
$ triggers interpolation. Escape it as $$ in compose.yaml, or single-quote the value in an env file.
Why do environment variables work at runtime but not during docker build?
Runtime variables are not available to build steps. Use build arguments for non-secret values, or make build commands independent of runtime settings.