Split Django Settings for Development and Production
Organise Django settings for development, staging, and production: a settings package with base, dev, and prod modules, environment variables for secrets, DJANGO_SETTINGS_MODULE per environment, and tests that keep production settings safe.
Problem statement
A single settings.py starts with DEBUG = True, a hard-coded SECRET_KEY, and SQLite. Going to production means changing those values, and the usual shortcuts cause real incidents: editing settings on the server (lost on the next deploy), if os.environ.get("PRODUCTION") branches scattered through the file, or a local_settings.py import that silently fails and leaves DEBUG on in production. A small, consistent structure keeps development convenient and production safe.
- Turn
settings.pyinto a package:config/settings/__init__.py,base.py,development.py,production.py(andtest.pyif needed). - Put everything shared in
base.py; each environment file starts withfrom .base import *. - In
production.py, read secrets and hosts from environment variables and fail if they are missing. - Set
DJANGO_SETTINGS_MODULE=config.settings.productionin the production environment; defaultmanage.pyto development. - Keep secrets out of all settings files; use environment variables or a secrets manager.
- Run
python manage.py check --deployagainst production settings in CI.
Step-by-step solution
1. Create the package
config/
├── settings/
│ ├── __init__.py
│ ├── base.py
│ ├── development.py
│ ├── production.py
│ └── test.py
├── urls.py
├── wsgi.py
└── asgi.py
Move the existing settings.py to base.py. Because base.py is now one directory deeper, fix BASE_DIR:
BASE_DIR = Path(__file__).resolve().parent.parent.parent
2. Shared and environment-specific settings
# config/settings/development.py
from .base import * # noqa: F401,F403
DEBUG = True
SECRET_KEY = "dev-only-not-secret"
ALLOWED_HOSTS = ["localhost", "127.0.0.1"]
DATABASES = {"default": {"ENGINE": "django.db.backends.postgresql", "NAME": "myapp_dev"}}
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
# config/settings/production.py
import os
import dj_database_url
from .base import * # noqa: F401,F403
DEBUG = False
SECRET_KEY = os.environ["SECRET_KEY"]
ALLOWED_HOSTS = os.environ["ALLOWED_HOSTS"].split(",")
CSRF_TRUSTED_ORIGINS = [f"https://{h}" for h in ALLOWED_HOSTS]
DATABASES = {"default": dj_database_url.config(conn_max_age=60, ssl_require=True)}
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000
os.environ["SECRET_KEY"] raises KeyError at startup if the variable is missing, which is exactly what you want: a loud failure instead of a silent insecure default.
3. Choose the module per environment
# manage.py
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.development")
# config/wsgi.py and config/asgi.py
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production")
Set DJANGO_SETTINGS_MODULE explicitly in every production process (systemd EnvironmentFile, Compose environment, Kubernetes env, PaaS config vars) so manage.py migrate, Celery, and cron jobs use production settings too, not the development default.
4. Avoid the classic traps
- No
try: from .local import *in production. A missing file silently changes behaviour. If you want local overrides, import them only indevelopment.py. - No secrets in any settings file, including staging. Use environment variables.
- No
if DEBUG:branches for security settings; set them explicitly per environment. - Do not use
development.pyon a public server, even temporarily.
5. Test production settings in CI
DJANGO_SETTINGS_MODULE=config.settings.production \
SECRET_KEY=ci-dummy ALLOWED_HOSTS=example.com DATABASE_URL=postgres://u:p@localhost/db \
python manage.py check --deploy --fail-level WARNING
This catches insecure production settings before they ship.
Verification check: on the server, python manage.py diffsettings --all | grep -E "^DEBUG|SECRET_KEY|ALLOWED_HOSTS" (with secret values masked in your terminal) shows production values; python manage.py check --deploy passes; starting the app without SECRET_KEY fails immediately.
Explanation
Django loads one settings module, named by DJANGO_SETTINGS_MODULE. A settings package with a shared base and thin environment modules uses plain Python imports to compose configuration, so it needs no extra library and is easy to read. Reading secrets and deployment-specific values from the environment follows the twelve-factor principle: the same code runs everywhere, and configuration that varies between deploys lives outside the code. Failing loudly on missing values turns configuration mistakes into deploy failures instead of security incidents.
Edge cases / notes
- Single settings file with env vars only is a valid alternative for small projects: one
settings.pywhere every environment-specific value comes from the environment. - django-environ or pydantic-settings add parsing and type casting; they combine well with this structure.
- Celery reads
DJANGO_SETTINGS_MODULEincelery.py; make sure it usessetdefaultso the environment wins. - Staging should use
production.pywith different environment values, not its own drifted settings file.
Next steps
- Parse environment variables cleanly with Configure Django Settings With django-environ.
- Review values with the Django Production Settings Checklist.
FAQ
What is the best way to split Django settings?
A settings package with base.py plus one module per environment, selected by DJANGO_SETTINGS_MODULE, with secrets from environment variables.
Should staging have its own settings file?
Usually not. Run staging on production settings with different environment values so it truly tests production behaviour.
Where should SECRET_KEY come from?
From an environment variable or secrets manager, never from a committed file.
How do I make manage.py use production settings on the server?
Set DJANGO_SETTINGS_MODULE=config.settings.production in the server environment.