Deployment
🚀 Deployment
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.
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:
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
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:
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.
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.
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.
collectstatic Before DeployLocally, 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.