Hook: Why settings matter in production
We've all seen a production incident caused by a debug setting left on, or a database URL hard-coded in source control. Django settings for multiple environments for production is where we stop relying on luck and start building resilient systems. The difference between a smooth deployment and a security breach often comes down to how we manage configuration across development, staging, and production.
The fundamental problem: One codebase, many environments
Our Django application needs to connect to different databases in development versus production. In development, we might use SQLite and have DEBUG enabled. In production, we're on PostgreSQL with DEBUG off, different caching layers, email backends, and third-party API keys. Hardcoding these values in settings.py is a non-starter—we'd need to edit the file on every deployment, and we'd be tempted to commit secrets to version control.
The solution is to externalize configuration so that the same codebase runs correctly across all environments. Environment variables become our source of truth, and our settings module reads from them dynamically.
Read secrets from environment variables
The simplest approach is to read environment variables directly using Python's os.environ. This is the baseline pattern every team should understand.
import os
# settings.py
SECRET_KEY = os.environ.get('SECRET_KEY')
DEBUG = os.environ.get('DEBUG', 'False') == 'True'
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', 'localhost').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'),
'HOST': os.environ.get('DB_HOST'),
'PORT': os.environ.get('DB_PORT', '5432'),
}
}
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': os.environ.get('REDIS_URL', 'redis://127.0.0.1:6379/1'),
}
}
This works, but has friction: we're repeating os.environ.get() constantly, we lack type validation, and handling optional vs. required variables becomes repetitive. For a small project, this is acceptable. For a team maintaining several microservices, we'll quickly want a better abstraction.
Django-environ: The traditional approach
The django-environ package has been the standard in Django teams for years. It provides a cleaner API and automatic type coercion.
import environ
# settings.py
env = environ.Env(
DEBUG=(bool, False),
SECRET_KEY=(str, ''),
ALLOWED_HOSTS=(list, []),
)
# Read from .env file (useful in dev) or environment
environ.Env.read_env()
SECRET_KEY = env('SECRET_KEY')
DEBUG = env('DEBUG')
ALLOWED_HOSTS = env('ALLOWED_HOSTS')
DATABASES = {
'default': env.db('DATABASE_URL')
}
CACHES = {
'default': env.cache('REDIS_URL')
}
The Env object handles casting automatically. If we define DEBUG=(bool, False), the string "true" becomes a Python boolean, and if the variable is missing, we get the default. We can parse connection URLs directly with env.db() and env.cache(), which is far cleaner than assembling dictionaries manually.
A typical .env file for development looks like:
DEBUG=True
SECRET_KEY=dev-secret-key-not-for-production
ALLOWED_HOSTS=localhost,127.0.0.1
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
REDIS_URL=redis://127.0.0.1:6379/1
The .env file goes in .gitignore. On production, we set environment variables directly in our deployment platform (AWS Systems Manager Parameter Store, Kubernetes ConfigMaps, Docker Compose, etc.), and django-environ reads them.
Pydantic settings: The modern alternative
Teams already using Pydantic for API validation often prefer pydantic-settings (formerly pydantic.settings in Pydantic v1). It offers stronger type safety and feels more Pythonic for teams comfortable with dataclass-like patterns.
from pydantic import Field
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
debug: bool = Field(default=False, alias='DEBUG')
secret_key: str = Field(alias='SECRET_KEY')
allowed_hosts: list[str] = Field(default=['localhost'], alias='ALLOWED_HOSTS')
database_url: str = Field(alias='DATABASE_URL')
redis_url: str = Field(default='redis://127.0.0.1:6379/1', alias='REDIS_URL')
class Config:
env_file = '.env'
case_sensitive = False
settings = Settings()
# In Django settings.py
SECRET_KEY = settings.secret_key
DEBUG = settings.debug
ALLOWED_HOSTS = settings.allowed_hosts
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
# Parse DATABASE_URL manually or use dj-database-url
}
}
Pydantic-settings excels when we want exhaustive validation. If SECRET_KEY is missing, we get a clear error at startup, not a silent None value. Type hints are first-class, so IDEs and type checkers understand our configuration. For teams using FastAPI alongside Django, Pydantic keeps the validation layer consistent across services.
The trade-off: Pydantic adds a dependency, and parsing complex structures like database connection pools requires more custom code than django-environ's built-in helpers.
Setting DEBUG=False in production
The single most critical setting for production safety is DEBUG=False. With DEBUG enabled in production, Django exposes your full traceback, local variables, SQL queries, and installed apps to anyone who triggers an error. This is a goldmine for attackers.
DEBUG = env('DEBUG', default=False)
if DEBUG:
ALLOWED_HOSTS = ['localhost', '127.0.0.1']
else:
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS') # Must be explicitly set
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
When DEBUG=False, we should also enable security headers. The snippet above enables HSTS, forces HTTPS, and ensures cookies are transmitted only over SSL. These settings are incompatible with development (where we're often on HTTP), so we gate them behind the DEBUG flag.
An additional pattern: use different settings modules for each environment.
myproject/
settings/
__init__.py
base.py # Shared configuration
development.py # Inherits from base, DEBUG=True
production.py # Inherits from base, DEBUG=False
# settings/base.py
import environ
env = environ.Env()
SECRET_KEY = env('SECRET_KEY')
INSTALLED_APPS = [...]
MIDDLEWARE = [...]
# settings/production.py
from .base import *
DEBUG = False
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS')
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 31536000
Then, in deployment, we pass DJANGO_SETTINGS_MODULE=myproject.settings.production. This makes environment-specific behavior explicit and easier to audit.
Run manage.py check --deploy
Django ships with a built-in deployment checklist. Running python manage.py check --deploy performs 30+ security and configuration checks, catching common mistakes before they reach production.
$ python manage.py check --deploy
SystemCheckError: System check identified some issues:
ERRORS:
?: (security.W004) SECURE_HSTS_SECONDS is not set to a recommended value.
HINT: Generally this should be set to at least 31536000 seconds (1 year).
?: (security.E014) SECURE_SSL_REDIRECT is not True. Redirecting to HTTPS should be enabled.
?: (admin.E408) 'django.contrib.admin' requires 'django.contrib.auth'.
We should integrate this check into our CI/CD pipeline, running it on every deployment:
#!/bin/bash
# deploy.sh
python manage.py check --deploy
if [ $? -ne 0 ]; then
echo "Deployment checks failed"
exit 1
fi
python manage.py migrate
gunicorn myproject.wsgi
This prevents accidental deployments with insecure settings. For local development, we can skip it or use a separate settings module that doesn't trigger warnings.
Practical example: A complete production setup
Here's how we'd set up a real team project with all three environments:
# settings.py (or settings/base.py)
import environ
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
env = environ.Env()
environ.Env.read_env(BASE_DIR / '.env')
SECRET_KEY = env('SECRET_KEY')
DEBUG = env('DEBUG', default=False)
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=['localhost'])
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'rest_framework',
'corsheaders',
'myapp',
]
DATABASES = {
'default': env.db('DATABASE_URL', default='sqlite:///db.sqlite3')
}
CACHES = {
'default': env.cache('REDIS_URL', default='locmem://')
}
if not DEBUG:
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
Local .env:
DEBUG=True
SECRET_KEY=local-dev-key
ALLOWED_HOSTS=localhost,127.0.0.1
DATABASE_URL=sqlite:///db.sqlite3
REDIS_URL=locmem://
Staging environment variables (e.g., in Kubernetes):
DEBUG=False
SECRET_KEY=
ALLOWED_HOSTS=staging.example.com
DATABASE_URL=postgresql://user:pass@staging-db:5432/mydb
REDIS_URL=redis://staging-redis:6379/0
When we deploy, we read these variables and run:
python manage.py check --deploy
python manage.py migrate
gunicorn myproject.wsgi:application --workers 4 --bind 0.0.0.0:8000
Common mistakes
- Committing .env files: Always add
.envto.gitignore. Use a template file like.env.exampleto document required variables without exposing secrets. - Forgetting to set SECRET_KEY in production: If
env('SECRET_KEY')returnsNone, Django will crash. Make it required:SECRET_KEY = env('SECRET_KEY')with no default. - Skipping the deployment check: Develop the habit of running
check --deploylocally before committing. Automate it in CI. - Mixing defaults in settings: If a value has no default and is optional per environment, document it clearly. Empty strings are often safer defaults than None.
- Not rotating secrets on deployment: Every environment should have unique keys. Reusing a production secret in staging is a liability if staging is compromised.
- Leaving DEBUG=True by accident: Use environment-based defaults and make DEBUG=False the production norm. Consider raising an exception if DEBUG is True on a production domain.
Key takeaways
- Read all environment-specific configuration from environment variables, never hardcode secrets or environment-dependent values in source code.
- Use
django-environfor straightforward projects orpydantic-settingsif you're already using Pydantic; both eliminate repetitive boilerplate. - Always set
DEBUG=Falsein production and enable security headers (SECURE_SSL_REDIRECT,SECURE_HSTS_SECONDS,SESSION_COOKIE_SECURE) in that configuration. - Run
python manage.py check --deploybefore every production deployment; integrate it into your CI/CD pipeline to catch misconfigurations early. - Consider splitting settings into base, development, and production modules to make environment-specific behavior explicit and auditable.
- Use a
.envfile for local development and set environment variables directly in your deployment platform (Kubernetes, Docker, AWS, etc.) for staging and production.






Comments
Loading comments…