Django environment variables are accessed via os.environ or the django-environ library for .env file support. The 12-factor app pattern says secrets go in env vars, not source. The team that uses django-environ has one config path for dev (.env file) and prod (real env vars). The team that hardcodes SECRET_KEY in settings.py has a security incident.
Table of contents
- The default pattern
- The django-environ pattern
- The .env file
- DJANGO_SETTINGS_MODULE
- Production secrets
- Common pitfalls
- FAQ
The default pattern
# settings.py
import os
SECRET_KEY = os.environ['DJANGO_SECRET_KEY']
DEBUG = os.environ.get('DJANGO_DEBUG', 'False') == 'True'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ.get('DB_NAME', 'mydb'),
'USER': os.environ.get('DB_USER', 'django'),
'PASSWORD': os.environ['DB_PASSWORD'],
'HOST': os.environ.get('DB_HOST', 'localhost'),
'PORT': os.environ.get('DB_PORT', '5432'),
}
}
The team that uses os.environ['KEY'] for required values crashes at startup if missing (good). The team that uses .get(..., default) for optional values is fine with defaults.
The django-environ pattern
import environ
env = environ.Env(
DEBUG=(bool, False)
)
# Reads from .env file in dev, env vars in prod
environ.Env.read_env() # loads .env
SECRET_KEY = env('DJANGO_SECRET_KEY')
DEBUG = env('DJANGO_DEBUG')
DATABASES = {
'default': env.db('DATABASE_URL', default='postgres://localhost/mydb')
}
The team that uses env.db('DATABASE_URL') parses PostgreSQL connection strings into Django’s DATABASES dict automatically.
The .env file
# .env (gitignored)
DJANGO_SECRET_KEY=dev-secret-key-only
DJANGO_DEBUG=True
DATABASE_URL=postgres://django:devpwd@localhost/mydb
Local dev loads this. Production uses real env vars (from systemd unit, Kubernetes pod spec, ECS task definition, etc.). Same code, different sources.
DJANGO_SETTINGS_MODULE
Django’s settings module can be selected per environment:
# Default
DJANGO_SETTINGS_MODULE=mysite.settings
# Production
export DJANGO_SETTINGS_MODULE=mysite.settings.production
The team that has separate settings/dev.py, settings/prod.py files has environment-specific overrides.
Production secrets
Don’t put secrets in env vars on bare metal where they leak to /proc/. Use:
- AWS ECS: ECS task role injects env vars from Secrets Manager.
- Kubernetes: External Secrets Operator pulls from AWS Secrets Manager / Vault.
- Bare metal: Vault Agent injects env vars at runtime.
The team that uses secret managers has rotating secrets, not static env files.
Common pitfalls
- Forgetting
DEBUG=Falsein production - leaks stack traces. - Hardcoding SECRET_KEY in settings.py - leaks on git push.
- Using SQLite in production because dev used it - no concurrency.
- Committing
.envto git - leaks secrets to history. - Not validating required env vars at startup - crashes at first request.
FAQ
Where do I put the .env file in Django?
Project root (same dir as manage.py). Add .env to .gitignore. The team that uses django-environ reads it automatically.
Do I need django-environ or is os.environ enough?
For simple projects, os.environ is fine. For complex ones with .env files, URL parsing, type casting - django-environ is worth the dep.
How do I handle different settings per environment?
Multiple settings files: settings/base.py, settings/dev.py, settings/prod.py. Set DJANGO_SETTINGS_MODULE to the right one. The team that has dev/prod parity (same env var names, different values) has cleaner config.
Should I commit .env.example?
Yes - .env.example (with placeholder values) shows what vars are needed without leaking real secrets. The team that uses .env.example has onboarding-friendly docs.
How do I validate env vars at startup?
Check required vars in settings.py: assert 'SECRET_KEY' in os.environ. Or use pydantic-settings for type-validated config. The team that validates catches config errors at deploy time, not runtime.
If you are sizing the infrastructure for the kind of project this post covers, the RunxBuild hosting calculator is the right place to model the line items. The compute, the memory, the storage, the bandwidth, the database - each one is a separate number, and the team’s mental model for the platform is the sum of those numbers. The RunxBuild dashboard is where the team sees the actual usage in one place.
Useful related references: