The recording for this lesson has not been published yet.
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 |
|---|---|---|
DEBUG | True | False |
DATABASES | SQLite | PostgreSQL |
EMAIL_BACKEND | console | SMTP |
SECURE_SSL_REDIRECT | False | True |
SESSION_COOKIE_SECURE | False | True |
| Extra apps | debug toolbar, django-extensions | none |
| Static files | served by runserver | WhiteNoise or Nginx |
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
DEBUG is convenient but blunt. The moment you add a
staging server that runs with DEBUG = False but without TLS, you will want a
separate ENVIRONMENT variable (local / staging /
production) instead.
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"
)
BASE_DIR is computed from __file__. When
settings.py becomes settings/base.py it sits one directory
deeper, so BASE_DIR needs one extra .parent. Forgetting this is
the number one reason the templates suddenly cannot be found.
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
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.
Every difference between development and production is a bug you cannot reproduce. Reduce the list on purpose:
docker compose up
(lesson 14.3).DEBUG = False at least once before every release and click
through the site.DEBUG is off.python manage.py check --deploy pass with zero warnings in the production
profile.django-debug-toolbar to the development profile only, and verify it
is not importable in production.DEBUG=False and
--insecure to see how static files behave.