Django’s settings.py can become a tangled mess of environment-specific configurations and hardcoded secrets, leading to security risks and deployment headaches.

Here’s a breakdown of how to manage your Django settings effectively across different environments (development, staging, production) without baking secrets directly into your code.

The Problem: A Single settings.py Becomes a Burden

Imagine your settings.py looking like this:

# settings.py (The Old Way)

DEBUG = True
ALLOWED_HOSTS = ['localhost', '127.0.0.1']
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'mydatabase',
        'USER': 'myuser',
        'PASSWORD': 'mypassword123!', # <-- BAD!
        'HOST': 'localhost',
        'PORT': '5432',
    }
}
SECRET_KEY = 'this-is-a-very-insecure-secret-key-for-dev' # <-- BAD!
EMAIL_HOST_PASSWORD = 'development_email_password' # <-- BAD!

# Production specific overrides (ugly and error-prone)
if os.environ.get('ENVIRONMENT') == 'production':
    DEBUG = False
    ALLOWED_HOSTS = ['.yourdomain.com']
    DATABASES = {
        'default': {
            'ENGINE': 'django.db.backends.postgresql',
            'NAME': 'prod_db_name',
            'USER': 'prod_user',
            'PASSWORD': 'prod_super_secret_password!', # <-- REALLY BAD!
            'HOST': 'prod_db_host.rds.amazonaws.com',
            'PORT': '5432',
        }
    }
    SECRET_KEY = 'this-is-the-production-secret-key-that-should-never-be-in-code' # <-- WORST!
    EMAIL_HOST_PASSWORD = 'production_real_email_password' # <-- VERY BAD!

This approach has several critical flaws:

  • Security Risks: Sensitive information like database passwords, API keys, and SECRET_KEY are directly in your codebase. Anyone with access to your repository can see them.
  • Deployment Nightmares: You have to manually edit settings.py for each environment, or rely on brittle if/else logic that’s hard to maintain. This is a prime source of "it works on my machine" issues.
  • Lack of Clarity: It’s difficult to quickly understand the configuration for a specific environment.

The Solution: Separate Settings and Use Environment Variables

The standard and most secure way to manage Django settings across environments is to:

  1. Create separate settings files for each environment (e.g., dev.py, staging.py, prod.py).
  2. Use environment variables to load the correct settings file and to inject sensitive values.

Let’s refactor.

Step 1: Project Structure

Organize your settings files within a settings directory in your project’s root, alongside your main settings.py (which will become a base file).

your_project/
├── manage.py
├── your_project/
│   ├── __init__.py
│   ├── urls.py
│   ├── wsgi.py
│   ├── asgi.py
│   └── settings/         <-- New directory
│       ├── __init__.py
│       ├── base.py       <-- Renamed from settings.py
│       ├── dev.py
│       ├── staging.py
│       └── prod.py
└── requirements.txt

Step 2: The Base Settings (base.py)

This file will contain all the common configurations that apply to all environments.

# your_project/settings/base.py

import os
from pathlib import Path
from dotenv import load_dotenv # For development

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent.parent

# Load environment variables from a .env file for local development
# This is optional but highly recommended for dev environments.
# Ensure you have python-dotenv installed: pip install python-dotenv
if os.environ.get('ENVIRONMENT') == 'development':
    load_dotenv(os.path.join(BASE_DIR, '.env'))

# --- Common Settings ---
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # Your other apps
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'your_project.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'your_project.wsgi.application'

# Database
# Will be overridden by specific environment settings
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}

# Password validation
AUTH_PASSWORD_VALIDATORS = [
    {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
    {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'},
    {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
    {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
]

LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True

STATIC_URL = 'static/'
STATIC_ROOT = BASE_DIR / 'staticfiles' # For production deployment

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

# --- Security ---
# SECRET_KEY is loaded from environment variable in specific settings files
# DEBUG is loaded from environment variable in specific settings files

Step 3: Environment-Specific Settings Files

These files will import from base.py and override specific settings, especially those related to secrets and environment-specific behaviors.

your_project/settings/dev.py

# your_project/settings/dev.py

from .base import *

# SECURITY WARNING: keep the secret key used in production secret!
# Load from environment variable or a .env file
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'fallback-insecure-secret-key-for-dev-only')

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.environ.get('DJANGO_DEBUG', 'True') == 'True' # Default to True if not set

ALLOWED_HOSTS = ['localhost', '127.0.0.1']

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': os.environ.get('DB_NAME', 'dev_db'),
        'USER': os.environ.get('DB_USER', 'dev_user'),
        'PASSWORD': os.environ.get('DB_PASSWORD', 'dev_password'), # Load from env
        'HOST': os.environ.get('DB_HOST', 'localhost'),
        'PORT': os.environ.get('DB_PORT', '5432'),
    }
}

# Example: Email settings (load from env)
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = os.environ.get('EMAIL_HOST', 'smtp.mailtrap.io')
EMAIL_PORT = int(os.environ.get('EMAIL_PORT', 587))
EMAIL_USE_TLS = os.environ.get('EMAIL_USE_TLS', 'True') == 'True'
EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER', 'your_mailtrap_username')
EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD', 'your_mailtrap_password') # Load from env
EMAIL_FROM_ADDRESS = os.environ.get('EMAIL_FROM_ADDRESS', 'noreply@example.com')

# For local development, you might want to use a tool like mailhog or mailtrap
# instead of sending real emails.
# EMAIL_BACKEND = 'anymail.backends.InbandEmailBackend' # Example for testing

your_project/settings/staging.py

# your_project/settings/staging.py

from .base import *

# Load from environment variables
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY')
DEBUG = os.environ.get('DJANGO_DEBUG', 'False') == 'True' # Default to False

ALLOWED_HOSTS = ['staging.yourdomain.com'] # Or get from env var

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': os.environ.get('DB_NAME'),
        'USER': os.environ.get('DB_USER'),
        'PASSWORD': os.environ.get('DB_PASSWORD'), # Load from env
        'HOST': os.environ.get('DB_HOST'),
        'PORT': os.environ.get('DB_PORT'),
    }
}

# Example: Email settings (load from env)
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = os.environ.get('EMAIL_HOST')
EMAIL_PORT = int(os.environ.get('EMAIL_PORT'))
EMAIL_USE_TLS = os.environ.get('EMAIL_USE_TLS') == 'True'
EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD') # Load from env
EMAIL_FROM_ADDRESS = os.environ.get('EMAIL_FROM_ADDRESS', 'noreply@staging.example.com')

your_project/settings/prod.py

# your_project/settings/prod.py

from .base import *

# Load from environment variables - CRITICAL FOR PRODUCTION
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY')
DEBUG = os.environ.get('DJANGO_DEBUG', 'False') == 'True' # Default to False

ALLOWED_HOSTS = os.environ.get('DJANGO_ALLOWED_HOSTS', '.yourdomain.com').split(',')

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': os.environ.get('DB_NAME'),
        'USER': os.environ.get('DB_USER'),
        'PASSWORD': os.environ.get('DB_PASSWORD'), # Load from env
        'HOST': os.environ.get('DB_HOST'),
        'PORT': os.environ.get('DB_PORT'),
    }
}

# Example: Email settings (load from env)
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = os.environ.get('EMAIL_HOST')
EMAIL_PORT = int(os.environ.get('EMAIL_PORT'))
EMAIL_USE_TLS = os.environ.get('EMAIL_USE_TLS') == 'True'
EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD') # Load from env
EMAIL_FROM_ADDRESS = os.environ.get('EMAIL_FROM_ADDRESS', 'noreply@yourdomain.com')

# Static files settings for production
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles' # Directory where collectstatic will gather files

# Caching (example)
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache',
        'LOCATION': os.environ.get('MEMCACHED_LOCATION', '127.0.0.1:11211'),
    }
}

# Logging configuration (important for production)
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
        },
    },
    'root': {
        'handlers': ['console'],
        'level': 'INFO',
    },
}

Step 4: Setting the DJANGO_SETTINGS_MODULE Environment Variable

Your application needs to know which settings file to load. This is controlled by the DJANGO_SETTINGS_MODULE environment variable.

  • Development:

    • If using .env files and python-dotenv: load_dotenv() in base.py will handle loading variables, but you still need to tell Django which settings file to use. You can set it in your shell or a manage.py wrapper.
    • Option A (Shell):
      export DJANGO_SETTINGS_MODULE=your_project.settings.dev
      python manage.py runserver
      
    • Option B (Wrapper Script - run_dev.sh):
      #!/bin/bash
      export DJANGO_SETTINGS_MODULE=your_project.settings.dev
      # Load .env for local development
      if [ -f .env ]; then
          export $(grep -v '^#' .env | xargs)
      fi
      python manage.py runserver
      
      Then run with ./run_dev.sh.
  • Staging/Production:

    • You must set this environment variable in your deployment environment (e.g., on your server, in your Dockerfile, in your CI/CD pipeline).
    • Example (Linux/macOS shell):
      export DJANGO_SETTINGS_MODULE=your_project.settings.prod
      # Set other required environment variables for production here
      export DJANGO_SECRET_KEY='your_actual_production_secret_key'
      export DB_NAME='your_prod_db_name'
      export DB_USER='your_prod_db_user'
      export DB_PASSWORD='your_prod_db_password'
      # ... and so on for all required variables
      gunicorn your_project.wsgi:application --bind 0.0.0.0:8000
      
    • In a Dockerfile:
      # ... your build steps ...
      ENV DJANGO_SETTINGS_MODULE=your_project.settings.prod
      # Set other ENV variables for production secrets
      ENV DJANGO_SECRET_KEY=${DJANGO_SECRET_KEY_FROM_BUILD_ARG}
      # ...
      CMD ["gunicorn", "your_project.wsgi:application", "--bind", "0.0.0.0:8000"]
      
      (You would pass DJANGO_SECRET_KEY_FROM_BUILD_ARG during docker build or set it as an environment variable when running the container).

Step 5: Managing Secrets

Never commit secrets to your version control system.

  • Development: Use a .env file in your project root. Add .env to your .gitignore file.

    # .env (for local development ONLY)
    DJANGO_SETTINGS_MODULE=your_project.settings.dev
    DJANGO_SECRET_KEY=your_development_secret_key_goes_here_change_me_often
    DEBUG=True
    
    DB_NAME=my_dev_db
    DB_USER=dev_user
    DB_PASSWORD=dev_password_super_secret
    DB_HOST=localhost
    DB_PORT=5432
    
    EMAIL_HOST=smtp.mailtrap.io
    EMAIL_PORT=587
    EMAIL_USE_TLS=True
    EMAIL_HOST_USER=your_mailtrap_username
    EMAIL_HOST_PASSWORD=your_mailtrap_password
    EMAIL_FROM_ADDRESS=noreply@dev.example.com
    

    The load_dotenv() call in base.py will load these variables into os.environ when DJANGO_SETTINGS_MODULE is set to your_project.settings.dev.

  • Staging/Production:

    • Environment Variables: This is the most common and recommended method. Your server (or container orchestration system like Kubernetes, Docker Swarm, or PaaS platforms like Heroku, AWS Elastic Beanstalk) should inject these variables directly into the application’s environment.
    • Secrets Management Tools: For more complex setups, consider dedicated secrets management solutions like HashiCorp Vault, AWS Secrets Manager, Google Secret Manager, or Azure Key Vault. You would then retrieve these secrets at runtime within your Django application (e.g., in your prod.py settings file or a custom settings loader).

The Result

Your settings/ directory now cleanly separates configurations:

  • base.py: The common ground.
  • dev.py, staging.py, prod.py: Environment-specific overrides.

Sensitive values are loaded from environment variables, meaning your codebase remains clean and secure. You can deploy with confidence by simply setting the DJANGO_SETTINGS_MODULE and other required environment variables.

The next logical step is to implement robust logging and error tracking for your production environment.

Want structured learning?

Take the full Django course →