Deployment

Django Intermediate/Advanced — Deployment
Django Intermediate/Advanced
Course 2 · Chapter 8 · Deployment

🚀 Deployment

Everything built across both Django courses needs to run somewhere real. This final chapter covers WSGI vs ASGI, why manage.py runserver must never touch production, static files (an area Django deliberately does not handle efficiently by design), and the environment-driven production settings that tie every prior chapter's loose ends together.

WSGI vs ASGI

Every Django project has generated both entry points since Course 1's startproject — this chapter is where they finally matter:

wsgi.py — Synchronous

The traditional entry point — one worker handles one request at a time. Fine for the vast majority of Django sites, including everything built in this course.

asgi.py — Asynchronous

Needed for async views, WebSockets, or Django Channels — genuinely concurrent request handling, closer to Node's natively async model.

Unlike Node, where async is the default, Django was WSGI-only for over a decade — ASGI and async view support were added later, which is why most existing Django deployments (and everything in this course) still run WSGI.

🖥️ Running Django in Production

runserver vs a Real Production Stack

manage.py runserver

Single-threaded, auto-reloading, serves static files itself when DEBUG=True — built entirely for local development. Django's own documentation explicitly warns against ever using it in production.

Gunicorn + Nginx

Gunicorn is a real WSGI application server — multiple worker processes, no auto-reload, no dev conveniences. Nginx sits in front as a reverse proxy, serving static files directly and forwarding dynamic requests to Gunicorn.

gunicorn mysite.wsgi:application --bind 0.0.0.0:8000 --workers 3

Static Files: collectstatic

Django's dev server serves CSS/JS/images automatically under DEBUG=True — but that convenience deliberately does not carry into production, where a real web server should serve static files instead:

# settings.py STATIC_URL = "/static/" STATIC_ROOT = BASE_DIR / "staticfiles" # where collectstatic gathers everything into
python manage.py collectstatic # Copies every app's static/ files (admin CSS, your own catalog/static/, ...) # into STATIC_ROOT as one combined directory nginx (or WhiteNoise) can serve directly.

Nginx Serving STATIC_ROOT

The typical production setup — Nginx serves /static/ directly from disk, never touching Django/Gunicorn for those requests at all.

WhiteNoise (No Nginx Needed)

A Python package letting Gunicorn itself serve static files efficiently — a simpler option for smaller deployments without a separate Nginx layer.

⚙️ Production Settings

Course 1's final chapter flagged DEBUG and ALLOWED_HOSTS as security-critical — deployment is where every environment-dependent setting needs a real, non-hardcoded source:

Environment-Driven settings.py

import os DEBUG = os.environ.get("DJANGO_DEBUG", "False") == "True" SECRET_KEY = os.environ["DJANGO_SECRET_KEY"] # raises KeyError immediately if missing — fail fast ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "").split(",") DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", "NAME": os.environ["DB_NAME"], "USER": os.environ["DB_USER"], "PASSWORD": os.environ["DB_PASSWORD"], "HOST": os.environ.get("DB_HOST", "localhost"), } }

Compare this to the TypeScript course's Configuration Management chapter — the intent is identical (never hardcode secrets, validate config exists before the app starts), but honestly, Django's approach is looser: os.environ["DJANGO_SECRET_KEY"] raises a plain KeyError on a missing variable rather than the schema-validated, typed failure a zod-based config module produces. Packages like django-environ or python-decouple add some structure, but nothing in the standard Django toolchain matches that chapter's compile-time-checked config shape.

Django's Built-in Deployment Checklist

A genuinely distinctive feature worth knowing about: Django ships its own automated check for common production misconfigurations:

python manage.py check --deploy # WARNINGS: # ?: (security.W004) You have not set a value for the SECURE_HSTS_SECONDS setting. # ?: (security.W008) Your SECURE_SSL_REDIRECT setting is not set to True. # ?: (security.W018) You should not have DEBUG set to True in deployment.

Running this before every deploy catches exactly the class of mistake Course 1's final chapter warned about — and several more — as an explicit, actionable list rather than something discovered after an incident.

💻 Coding Challenges

Challenge 1: Write Environment-Driven Settings

Rewrite a settings.py with hardcoded DEBUG = True, a hardcoded SECRET_KEY string, and an empty ALLOWED_HOSTS to instead read all three from environment variables, with SECRET_KEY failing fast if it's missing.

Goal: Practice moving hardcoded, environment-dependent values out of source-controlled settings.

→ Solution

Challenge 2: Write a Gunicorn + Static Files Deployment Sequence

Write the ordered shell commands needed to deploy an update to a running Django app: installing dependencies, running migrations, collecting static files, and finally restarting Gunicorn.

Goal: Practice the real deployment sequence, including why collectstatic must run before Gunicorn restarts.

→ Solution

Challenge 3: Interpret a check --deploy Warning

Given the warning "security.W018: You should not have DEBUG set to True in deployment," explain what setting needs to change and why check --deploy is a better safety net than relying on a developer remembering to change it manually before every deploy.

Goal: Practice treating Django's own deployment checklist as a real pre-deploy gate, not just informational output.

→ Solution

⚠️ Gotcha: Forgetting collectstatic Before Deploy

Locally, runserver under DEBUG=True serves every static file automatically — CSS and JS just work, with no extra step. Deploy without running collectstatic first, and every one of those files silently 404s in production: pages load, but with no styling and no interactivity, often looking like a completely broken deployment rather than the one missing command it actually is. Equally serious: never commit SECRET_KEY to source control — Course 1's Security Misconfiguration warning applies directly here, since a leaked SECRET_KEY can be used to forge session cookies and other signed data.

🎓 Course Complete

That closes Django Intermediate/Advanced — and with it, the full Django project (16 chapters across both courses). Course 1 built the batteries-included fundamentals against FastAPI and Express; Course 2 layered on everything a real application needs beyond CRUD: authentication built on the framework's own tested system, a full REST API, a real test suite with automatic per-test isolation, decoupled middleware and signals, background work that doesn't block a response, caching with deliberate invalidation, generic class-based views, and finally a real, production-hardened deployment. The same thread ran through both courses: Django's answer to nearly every question is "it's already built in, and it's been reviewed by a large community for years" — the batteries-included philosophy, all the way to the last chapter.