Video coming soon

The recording for this lesson has not been published yet.

What we cover

  • One settings file with environment switches
  • A settings package: base, dev, prod
  • DJANGO_SETTINGS_MODULE in manage.py and wsgi.py
  • Keeping the two environments as similar as possible

Lesson notes

1. Why one settings file is not enough

On your laptop you want SQLite, a console e-mail backend, the debug toolbar and tracebacks in the browser. In production you want PostgreSQL, real SMTP, HTTPS redirects, HSTS and absolutely no tracebacks. Those are not two values - they are two profiles.

Setting Development Production
DEBUGTrueFalse
DATABASESSQLitePostgreSQL
EMAIL_BACKENDconsoleSMTP
SECURE_SSL_REDIRECTFalseTrue
SESSION_COOKIE_SECUREFalseTrue
Extra appsdebug toolbar, django-extensionsnone
Static filesserved by runserverWhiteNoise or Nginx

2. Two ways to do it

A - one file with switches core/settings.py if DEBUG: ... else: ... .env decides the profile simple, one place to look grows into a maze of ifs B - a settings package core/settings/base.py dev.py prod.py DJANGO_SETTINGS_MODULE picks one explicit, easy to diff two files can drift apart

3. Approach A: one file, driven by the environment

This is what small projects (including this website) use. Everything is read from the environment, and the few genuinely conditional blocks are grouped at the bottom.

# core/settings.py
DEBUG = config("DEBUG", default=False, cast=bool)

DATABASES = {
    "default": {
        "ENGINE": config("DATABASE_ENGINE", default="django.db.backends.sqlite3"),
        "NAME": config("DATABASE_NAME", default=BASE_DIR / "db.sqlite3"),
        "USER": config("DATABASE_USERNAME", default=""),
        "PASSWORD": config("DATABASE_PASSWORD", default=""),
        "HOST": config("DATABASE_HOST", default="localhost"),
        "PORT": config("DATABASE_PORT", default="5432"),
    }
}

if DEBUG:
    EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
    INSTALLED_APPS += ["debug_toolbar", "django_extensions"]
    MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware")
    INTERNAL_IPS = ["127.0.0.1"]
else:
    SECURE_SSL_REDIRECT = True
    SECURE_HSTS_SECONDS = 60 * 60 * 24 * 365
    SECURE_HSTS_INCLUDE_SUBDOMAINS = True
    SESSION_COOKIE_SECURE = True
    CSRF_COOKIE_SECURE = True

4. Approach B: a settings package

Turn the module into a package:

core/
└── settings/
    ├── __init__.py     # empty
    ├── base.py         # everything both environments share
    ├── dev.py          # from .base import *  + local overrides
    └── prod.py         # from .base import *  + hardening
# core/settings/dev.py
from .base import *  # noqa: F403

DEBUG = True
ALLOWED_HOSTS = ["127.0.0.1", "localhost"]

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": BASE_DIR / "db.sqlite3",  # noqa: F405
    }
}

EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"

INSTALLED_APPS += ["debug_toolbar"]  # noqa: F405
MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware")  # noqa: F405
INTERNAL_IPS = ["127.0.0.1"]
# core/settings/prod.py
from .base import *  # noqa: F403

DEBUG = False
ALLOWED_HOSTS = config("DJANGO_ALLOWED_HOSTS", cast=Csv())  # noqa: F405

SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 60 * 60 * 24 * 365
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")

STORAGES["staticfiles"]["BACKEND"] = (  # noqa: F405
    "whitenoise.storage.CompressedManifestStaticFilesStorage"
)

5. Telling Django which one to use

DJANGO_SETTINGS_MODULE is the switch. It is read in three places, and all three must agree.

# manage.py
import os
import sys


def main():
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings.dev")
    ...
# core/wsgi.py  (and asgi.py)
import os

from django.core.wsgi import get_wsgi_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings.prod")

application = get_wsgi_application()

Note setdefault: an explicitly exported variable always wins, so you can override the choice from the shell without editing a single file.

python manage.py runserver                                    # uses core.settings.dev
DJANGO_SETTINGS_MODULE=core.settings.prod python manage.py check --deploy
python manage.py migrate --settings=core.settings.prod

6. Which approach should you pick?

  • One file + environment variables - a personal blog, a single server, a team of one or two. Fewer files, nothing hidden by a star import.
  • Settings package - more than two environments, or when dev-only dependencies must never be importable in production.

Both are legitimate. What is not legitimate is a settings_local.py that everyone edits by hand and nobody commits: it makes every machine subtly different, and "works on my laptop" becomes a way of life.

7. Keep the two environments close

Every difference between development and production is a bug you cannot reproduce. Reduce the list on purpose:

  • Run PostgreSQL locally too - in Docker it costs one docker compose up (lesson 14.3).
  • Run with DEBUG = False at least once before every release and click through the site.
  • Pin the same Python and Django versions everywhere.
  • Test your 404 and 500 templates - they only appear when DEBUG is off.

8. Exercise

  1. Convert your project to whichever approach you prefer, and make python manage.py check --deploy pass with zero warnings in the production profile.
  2. Add django-debug-toolbar to the development profile only, and verify it is not importable in production.
  3. Run the development server once with DEBUG=False and --insecure to see how static files behave.
  4. Write down the remaining differences between your two environments - the shorter the list, the better you sleep.

Further reading

After this lesson you will

  • Choose between env switches and a settings package
  • Run the same project with two configurations