skip to content

Running in Production

What it takes to run a Django project for real: WSGI and ASGI servers, the pre-launch settings sweep and the commands a release runs. Interviewers probe the gap between runserver and production.

part ofDjangooverview, primer and where to startread it →
on this pageshow

explore

questions

14

Why must DEBUG be False on a production Django site, and what stops working the moment you turn it off?

level: juniorimportance: must knowfreq 80%

answer

  1. what an error page reveals
  2. tracebacks, local variables, settings
  3. the host check stops being lenient
  4. static files and error templates

basics

~20 s

DEBUG=True shows tracebacks with local variables, request data and most settings to anyone who triggers an error, and keeps a SQL log in memory. Turning it off requires a real ALLOWED_HOSTS, another way to serve static files, and 404/500 templates.

solid answer

~40 s

With `DEBUG = True` any error renders Django's technical page: the traceback, local variables of each frame, request data and the settings, with only values whose names look sensitive (`KEY`, `SECRET`, `PASS`, `TOKEN` and similar) masked; a 404 lists your URL patterns. Django also logs every SQL query per connection, up to 9,000, and `check --deploy` flags it as `security.W018`. Note the trap: the global default is `False`, but `startproject` writes `DEBUG = True`. When you turn it off, three things change at once: an empty `ALLOWED_HOSTS` stops falling back to localhost names, so every request gets a 400; the staticfiles `runserver` handler stops serving static files; and errors render your `404.html`/`500.html` templates or Django's plain defaults, while the default logging config starts emailing errors to `ADMINS`.

code

python · 4 lines
python
import os

DEBUG = os.environ.get('DJANGO_DEBUG') == '1'
ALLOWED_HOSTS = [h for h in os.environ.get('DJANGO_ALLOWED_HOSTS', '').split(',') if h]

go deeper

for a junior

Know that DEBUG must be False in production because error pages leak code and data, and that ALLOWED_HOSTS must then list your domains.

for a middle

Explain everything DEBUG toggles: the technical error pages, the SQL log, the host fallback, static serving under runserver and the default logging behaviour.

for a senior

Make debug mode impossible to ship by accident: environment-driven settings defaulting to off, check --deploy in CI, and error reporting that replaces the debug page.

for a principal

Set an organisation-wide rule for production settings, where safe defaults live in code and every deviation must be explicit and reviewed.

## What DEBUG = True exposes `DEBUG` is the switch between Django's developer conveniences and its production behaviour. With it on, Django helps whoever is looking at the screen — which in production means anyone on the internet. - **Technical 500 page.** An unhandled exception renders the full traceback, the **local variables** of every frame, the request's GET, POST, cookies and META, and the project settings. Settings whose names match `API`, `AUTH`, `TOKEN`, `KEY`, `SECRET`, `PASS`, `SIGNATURE` or `HTTP_COOKIE` are masked; everything else — and every local variable — is shown as is. - **Technical 404 page.** A missing URL lists the URL patterns Django tried, which maps your whole application for an attacker. - **SQL query log.** Every query on every connection is recorded in `connection.queries`, up to 9,000 per connection, costing memory and time on every request. - **Relaxed host check.** With `ALLOWED_HOSTS` empty, Django accepts `localhost` variants so development just works. The deployment checks report `DEBUG = True` as **`security.W018`**: *You should not have DEBUG set to True in deployment.* ## The default trap Django's **global default is `DEBUG = False`**, but the `settings.py` that `startproject` generates sets **`DEBUG = True`**. A project that ships that file unchanged ships debug mode. The usual fix is to read `DEBUG` from the environment so that production can only turn it on deliberately. ## What changes when you turn it off | Area | With `DEBUG = True` | With `DEBUG = False` | |---|---|---| | unhandled exception | technical 500 page | your `500.html` template, or a plain default page | | unmatched URL | technical 404 listing URL patterns | your `404.html` template, or a plain default page | | empty `ALLOWED_HOSTS` | localhost names accepted | every request rejected with 400 Bad Request | | static files under `runserver` | served by the staticfiles handler | not served (unless `--insecure`) | | default `django` logger to console | INFO and above printed | discarded | | error emails to `ADMINS` | not sent | ERROR records emailed by the default config | | SQL query log | kept in memory | not kept (unless forced) | ## The launch checklist this implies 1. Set `DEBUG = False`, ideally from an environment variable that defaults to off. 2. Set `ALLOWED_HOSTS` to the domains you serve; leaving it empty turns every request into a 400. 3. Serve static files through something other than `runserver` after running `collectstatic`. 4. Add `404.html` and `500.html` templates; the 500 template is rendered without a request context, so it should not depend on context processors. 5. Decide where errors go now that the debug page is gone: `ADMINS` for email, and a `LOGGING` configuration that writes to the output your platform collects. 6. Run `manage.py check --deploy` against the production settings to catch the rest. ## Common misunderstandings - Turning `DEBUG` off does **not** make Django "faster" by itself in any dramatic way; the security exposure is the reason. - Masking only covers *settings* by name. A local variable called `card_number` in a view frame is displayed in full. - A site that works in staging with `DEBUG = True` and breaks with 400s in production is almost always an `ALLOWED_HOSTS` problem. ## What interviewers listen for The leak (tracebacks, locals, settings, URL map), the generated-template trap, and the concrete follow-on work: `ALLOWED_HOSTS`, static files, error templates and error reporting.

  • After setting DEBUG = False, every request returns 400 Bad Request. What is wrong?
    `ALLOWED_HOSTS` is empty or does not include the host clients use. With `DEBUG` on, an empty list falls back to localhost names; with it off, Django validates the `Host` header strictly and raises `DisallowedHost`, which becomes a 400. Add the production domain names to `ALLOWED_HOSTS`.
  • If Django masks sensitive settings on the debug page, why is DEBUG = True still dangerous?
    Masking works only on setting names that match patterns such as `KEY`, `SECRET` or `PASS`. Local variables in every traceback frame, request POST data, cookies and headers, non-matching settings and the full URL map are shown as is. One error page can reveal user data, internal hostnames and credentials held in ordinary variables.

saying these in an interview costs you the question

  • DEBUG defaults to True, so it only needs turning off once
  • The debug page masks all secrets, so leaving DEBUG on is only a cosmetic issue
  • An empty ALLOWED_HOSTS accepts any host when DEBUG is False
  • DEBUG = True stores an unlimited number of SQL queries per connection
  • Turning DEBUG off keeps runserver serving static files as before
open as a page

Why is Django's runserver command not meant for production, and what serves a Django project there instead?

level: juniorimportance: must knowfreq 70%

basics

~20 s

runserver is a development server: it auto-reloads, runs in one process and was never security-audited or performance-tested. Production runs the project's wsgi.py or asgi.py application object under a real WSGI or ASGI server, usually behind a reverse proxy.

open as a page

In a containerised Django deployment, why does collectstatic run when the image is built while migrate runs once per release before new code takes traffic?

level: middleimportance: must knowfreq 62%

basics

~20 s

collectstatic reads files already in the code and writes them to STATIC_ROOT, so its output belongs in the immutable image. migrate alters the shared database, so it runs once per release, before new-code containers serve requests.

open as a page

What do a Django project's wsgi.py and asgi.py contain, and how does DJANGO_SETTINGS_MODULE decide which settings they load?

level: middleimportance: must knowfreq 55%

basics

~10 s

Each file sets a default DJANGO_SETTINGS_MODULE with os.environ.setdefault and builds a module-level application via get_wsgi_application() or get_asgi_application(). A value already in the environment wins, so each deployment picks its settings without editing the file.

open as a page

How do you run Django management commands such as migrate, collectstatic and createsuperuser non-interactively inside a container?

level: juniorimportance: should knowfreq 48%

basics

~20 s

Pass --noinput (or --no-input) so no command waits for a prompt, give createsuperuser its values through options or DJANGO_SUPERUSER_* environment variables, and rely on the exit status: a failing command exits non-zero and fails the step.

open as a page

Before a Django launch, what do the defaults already do for CONN_MAX_AGE and the cached template loader, and when do you change them?

level: middleimportance: should knowfreq 35%

basics

~20 s

CONN_MAX_AGE defaults to 0, opening a database connection per request; under WSGI a positive value with CONN_HEALTH_CHECKS reuses it. The cached template loader is already on unless OPTIONS['loaders'] is set, in which case you wrap the loaders yourself.

open as a page

Why can a Django collectstatic step fail during an image build, and how do you make the settings importable there without production secrets?

level: middleimportance: should knowfreq 38%

basics

~20 s

collectstatic imports the whole settings module and every installed app, so settings that demand runtime secrets, or ready() hooks that query the database, break the build. Give that command placeholder values, keep production's STORAGES backend, and keep queries out of import time.

open as a page

With DEBUG off, how does a Django project report unhandled exceptions by default, and what must you configure before launch so errors are not lost?

level: seniorimportance: should knowfreq 40%

basics

~20 s

By default Django emails ERROR records from its loggers, including unhandled view exceptions, to ADMINS when DEBUG is False, and discards the rest. Set ADMINS, SERVER_EMAIL and a real mail backend, and add a LOGGING config that writes to collected output.

open as a page

Reviewing a startup's first production deploy of a Django app, which HTTPS and cookie settings do you require, and why roll out HSTS gradually?

level: seniorimportance: should knowfreq 50%

basics

~10 s

Require SECURE_SSL_REDIRECT (or a proxy redirect), SESSION_COOKIE_SECURE and CSRF_COOKIE_SECURE, plus SECURE_PROXY_SSL_HEADER behind a TLS-terminating proxy. Enable HSTS with a small SECURE_HSTS_SECONDS first, because browsers cache the policy and a mistake cannot be recalled.

open as a page

A Django health-check view answers an orchestrator's HTTP probe with 400 or a redirect although the app is healthy; which Django settings cause this, and how do you fix it?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Probes often send the container's IP as Host, which ALLOWED_HOSTS rejects with 400, and plain HTTP, which SECURE_SSL_REDIRECT answers with a 301. Send an allowed Host, exempt the path with SECURE_REDIRECT_EXEMPT, and mark the view login_not_required.

open as a page

When sizing worker processes and threads for a Django deployment, how does Django's one-connection-per-thread model limit the numbers?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Django opens a separate database connection in every thread that queries, so each worker thread in each process and instance can hold one. The database's connection limit therefore caps processes times threads, and persistent connections keep idle workers' connections open too.

open as a page

A Django project has mostly sync views plus a few async views that call slow external APIs; should you serve it with WSGI or ASGI?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Under WSGI an async view runs in its own one-off event loop while holding a worker, so awaits inside one view overlap but requests do not. ASGI pays off when async-capable middleware reaches views that stream or wait long on upstream I/O.

open as a page

How can a Django release pipeline use migrate --plan and migrate --check to preview and gate pending migrations before new code takes traffic?

level: seniorimportance: nice to knowfreq 26%

basics

~20 s

migrate --plan prints the migrations and operations that would run without applying them; migrate --check applies nothing and exits non-zero while unapplied migrations exist. Review with the first, and gate traffic or container start on the second.

open as a page

When a reverse proxy serves a Django project under /shop/, how does Django learn that prefix so reverse() and redirects include it?

level: seniorimportance: nice to knowfreq 20%

basics

~20 s

Django takes a script prefix per request from the WSGI SCRIPT_NAME or the ASGI root_path, or from FORCE_SCRIPT_NAME when set, and reverse() prepends it. The server must pass the prefix that way rather than baking /shop/ into the URLconf.

open as a page