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:8000

Important: .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:
FormSourceWhat happens if missing
KEY: valueHardcoded in fileAlways set
KEY: ${VAR}Host shell or .envEmpty string or warning
KEY: (no value)Host shell onlyUnset 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.com

When 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

MechanismScopeUsed for
.env fileCompose YAML onlyTemplating compose file values (image tags, port numbers)
environment:Container runtimeInjecting config and secrets into the running process
env_file:Container runtimeSame as environment: but loaded from a file

See Also