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_KEYare directly in your codebase. Anyone with access to your repository can see them. - Deployment Nightmares: You have to manually edit
settings.pyfor each environment, or rely on brittleif/elselogic 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:
- Create separate settings files for each environment (e.g.,
dev.py,staging.py,prod.py). - 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
.envfiles andpython-dotenv:load_dotenv()inbase.pywill handle loading variables, but you still need to tell Django which settings file to use. You can set it in your shell or amanage.pywrapper. - Option A (Shell):
export DJANGO_SETTINGS_MODULE=your_project.settings.dev python manage.py runserver - Option B (Wrapper Script -
run_dev.sh):
Then run with#!/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./run_dev.sh.
- If using
-
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:
(You would pass# ... 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"]DJANGO_SECRET_KEY_FROM_BUILD_ARGduringdocker buildor 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
.envfile in your project root. Add.envto your.gitignorefile.# .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.comThe
load_dotenv()call inbase.pywill load these variables intoos.environwhenDJANGO_SETTINGS_MODULEis set toyour_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.pysettings 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.