Deployment

Food Tracker (Django)

Chapter 12 · Deployment

Every earlier chapter's "batteries included" moments were real. This one is where that story honestly stops covering everything.

DEBUG=False: The Single Most Important Flag

Django's development default, DEBUG=True, shows full, detailed error pages — stack traces, local variable values, even source snippets. Genuinely useful while developing; genuinely dangerous left on in production, since it hands anyone who triggers an error a real window into secrets, internal paths, and database structure. DEBUG=False swaps that for a generic error page instead — but it also activates a real requirement that trips up nearly every first-time Django deployment:

# settings.py (production) DEBUG = False ALLOWED_HOSTS = ["foodtracker.example.com"] SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]

With DEBUG=False, Django refuses to serve any request at all if its Host header doesn't match something in ALLOWED_HOSTS — everything that worked perfectly with runserver locally can appear completely broken the moment DEBUG flips to False, purely because ALLOWED_HOSTS was never configured for the real domain.

SECRET_KEY: This Course's First Genuine Secret

Every external API this course has touched — Open Food Facts, TheMealDB — never needed a real secret at all. Django's own SECRET_KEY is different: it signs sessions, CSRF tokens (Chapter 6's own protection depends on it), and password-reset tokens. If it leaks, an attacker can forge valid session cookies directly. It must be loaded from an environment variable, never hardcoded or committed to source control — the one place in this entire course where an actual secret genuinely exists, and it belongs to Django itself, not to anything this app integrates with.

Static Files vs. Media Files

Static files are the app's own assets — CSS, and Chapter 5's own scan.js. Media files would be user-uploaded content (this app has none currently, but the distinction is worth knowing since it's a common point of confusion). python manage.py collectstatic gathers every app's static files into one directory for production serving — a step Django's own dev server never required, since it serves static files automatically during development without it.

Django itself explicitly does not serve static files efficiently in production. A real deployment needs either a dedicated web server (nginx) in front, or a library like WhiteNoise letting the Django app serve them itself reasonably well — a genuine decision point this chapter names rather than glosses over.

SQLite in Production: An Honest, Calibrated Note

Chapter 2 called SQLite the genuine, appropriate choice for this app, not a placeholder — and for this app's own realistically small, personal-use scope, that can remain true in production too. The honest caveat: SQLite's own file-level locking becomes a real concurrency bottleneck under genuine multi-user, high-write-volume load. If this were ever a shared, heavily-used deployment, PostgreSQL is the standard upgrade path — not because SQLite is "never production-ready" (a common overstatement), but because its real limits are concurrency-shaped, and worth knowing about honestly rather than either dismissing or overselling.

Running It For Real

pip install gunicorn gunicorn foodtracker.wsgi:application

Chapter 1's manage.py runserver was always explicitly a development-only server — Django's own documentation says directly not to use it in production, since it offers neither real concurrency nor production security hardening. Gunicorn (or another genuine WSGI server) is what actually serves production traffic.

Where "batteries included" honestly stops
Django ships an ORM, an admin, forms, and — as of Chapter 11 — a REST framework, all genuinely included. It does not ship a production-grade web server, an efficient static-file server, or a database that scales to arbitrary concurrent load. No framework's "batteries included" promise extends infinitely — this chapter is simply the honest, unavoidable place that becomes visible.
Let Django check its own configuration
python manage.py check --deploy scans the current settings for common production misconfigurations — DEBUG still True, missing SECURE_* settings, and more — worth running before trusting a deployment is actually ready.
The classic first-deployment mistake
Flipping DEBUG to False without also setting ALLOWED_HOSTS correctly makes the entire site appear to stop working — every request gets rejected outright, with no obvious explanation unless the connection between these two settings is already understood.

Where This Course Is Headed

One chapter left: a capstone tying together this course's own thread — from Chapter 1's batteries-included framing through this chapter's own real production considerations — into one complete, working app.

Hands-On Exercises

Exercise 1

Explain why DEBUG=True is appropriate in development but a real security risk in production, and explain exactly what happens (and why) once DEBUG=False if ALLOWED_HOSTS hasn't been configured for the real domain.

📄 View solution
Exercise 2

Explain why SECRET_KEY is a genuine secret in a way Open Food Facts and TheMealDB never required, and name at least two things it's actually used to protect.

📄 View solution
Exercise 3

Explain why manage.py runserver and SQLite were both genuinely appropriate choices for earlier chapters, and why deployment is specifically where each one's own real limits actually become visible.

📄 View solution

Chapter 12 Quick Reference

  • DEBUG=False + ALLOWED_HOSTS — the classic first-deployment mistake if not set together
  • SECRET_KEY — this course's first genuine secret; signs sessions, CSRF tokens, password resets
  • collectstatic — a real production-only step; the dev server never needed it
  • SQLite in production — genuinely fine for this app's own scope; PostgreSQL is the real upgrade path for concurrency, not a blanket "never production-ready" rule
  • Gunicorn, not runserverrunserver was always explicitly dev-only
  • Next chapter: Capstone — A Complete, Working Food Tracker