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.

How variables reach Django.env filecompose interpolationcompose.yamlenvironment, env_fileContainer envset at creationos.environread by settingsThe project .env feeds the compose file; only environment and env_file reach the container.
  1. The .env file in the project directory is used to substitute ${VAR} inside compose.yaml. It is not passed into containers by itself.
  2. To pass variables into a container, list them under environment: or reference a file with env_file:.
  3. Check what Compose resolved with docker compose config, and what the container has with docker compose exec web env.
  4. After changing variables, run docker compose up -d to recreate containers; docker compose restart keeps the old environment.
  5. Quote values containing $ with single quotes in env files, and escape $ as $$ inside compose.yaml.
  6. 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:
same name, two different jobsUsed forReaches Django?.env next to compose.yaml${VAR} substitution in the file!only if referencedenv_file: in a servicethe container environment✓yesA variable in .env alone never reaches the containerdjango-deployment.com
docker compose config shows substitution; exec env shows the container.
# 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:

  1. docker compose run -e VAR=value (one-off containers)
  2. environment: in compose.yaml (with values interpolated from the shell or .env)
  3. env_file: entries
  4. ENV in the Dockerfile
who wins when a variable is set twicedocker compose run -ehighestenvironment: in the serviceoverrides env_fileenv_file: fileslater files override earlierDockerfile ENVlowest: image defaultsAn empty environment: entry can silently override a good env_file valuedjango-deployment.com
DEBUG=${DEBUG} with DEBUG unset in the shell becomes an empty string.

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.

build time is not run timeBuild time (ARG)build: args: in Compose✕env_file is not available here✕baked into layers: no secretsRun time (environment)✓env_file and environment:✓read by Django at start-up✓change with up -d, no rebuildcollectstatic in the Dockerfile cannot see env_file valuesdjango-deployment.com
Give build steps harmless placeholder values instead.

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: under build: in Compose, ARG in 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

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.

Learning path · step 6 of 12

Docker, from image to production

Keep going