Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild

Django Environment Variables: django-environ, os.environ, and 12-Factor

Sean

Platform Writer

Jul 05, 2026
6 min read

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.

Django Environment Variables: django-environ, os.environ, and 12-Factor

Table of contents

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

  1. Forgetting DEBUG=False in production - leaks stack traces.
  2. Hardcoding SECRET_KEY in settings.py - leaks on git push.
  3. Using SQLite in production because dev used it - no concurrency.
  4. Committing .env to git - leaks secrets to history.
  5. 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:

#django#env#config#dev-infra