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.

A settings packagebase.pyshared settingsdevelopment.pyDEBUG, local DBproduction.pysecure, from envSettings moduleenv var picks oneShared settings live once; each environment overrides what differs and reads secrets from env.
  1. Turn settings.py into a package: config/settings/__init__.py, base.py, development.py, production.py (and test.py if needed).
  2. Put everything shared in base.py; each environment file starts with from .base import *.
  3. In production.py, read secrets and hosts from environment variables and fail if they are missing.
  4. Set DJANGO_SETTINGS_MODULE=config.settings.production in the production environment; default manage.py to development.
  5. Keep secrets out of all settings files; use environment variables or a secrets manager.
  6. Run python manage.py check --deploy against 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.

how the pieces combineEnvironment variablessecrets, hosts, URLsproduction.pyDEBUG off, security settingsdevelopment.py / test.pylocal conveniencesbase.pyapps, middleware, templatesEach layer overrides only what differs from the one belowdjango-deployment.com
Code shared by all environments lives once, in base.py.

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 in development.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.py on 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.

common settings mistakes and their fixRisky patterns✕optional local_settings import✕default SECRET_KEY in code✕editing settings on the serverSplit package✓explicit module per environment✓required env vars, fail loudly✓check --deploy in CIProduction settings should be impossible to load with insecure valuesdjango-deployment.com
Failing at startup is far better than running with DEBUG on.

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.py where 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_MODULE in celery.py; make sure it uses setdefault so the environment wins.
  • Staging should use production.py with different environment values, not its own drifted settings file.

Next steps

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.

Keep going