Deploying Django: production settings and going live
Twenty-one parts in, the blog has models, an admin, full CRUD, auth, permissions, uploads, pagination, and tests. What it doesn't have yet is a public URL. Every setting in this part exists because runserver's defaults are deliberately unsafe for anything but a laptop.
Getting SECRET_KEY and DEBUG out of settings.py
# blogsite/settings.py
import os
from pathlib import Path
SECRET_KEY = os.environ["SECRET_KEY"]
DEBUG = os.environ.get("DEBUG", "False") == "True"django-admin startproject generates a SECRET_KEY hardcoded directly in settings.py, checked into git along with everything else — fine for following along with this series, not fine the moment settings.py is public in a repository. os.environ["SECRET_KEY"] (no default) means the app refuses to start at all without one set — a deliberate failure instead of silently falling back to an insecure default in production. DEBUG defaults to False the same way: opting into debug mode has to be explicit, since DEBUG = True in production leaks full stack traces, including settings values, to anyone who triggers an error.
ALLOWED_HOSTS
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "").split(",")Django refuses to serve a request whose Host header isn't in this list — a real protection against a category of attack that spoofs the Host header to trick a misconfigured app into generating malicious links or emails. In production, set ALLOWED_HOSTS=yourdomain.com as an environment variable; .split(",") supports more than one if needed.
Serving static files without a separate service
pip install whitenoise# blogsite/settings.py
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
# ... the rest, unchanged ...
]
STATIC_ROOT = BASE_DIR / "staticfiles"
STORAGES = {
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}Part 18 mentioned that DEBUG = False turns off Django's dev-server static file serving — WhiteNoise is the standard replacement that doesn't require a separate static file host or CDN. It has to sit directly after SecurityMiddleware in the list, and collectstatic (from part 18, now actually required) gathers every static file into STATIC_ROOT for it to serve.
Switching the database
pip install dj-database-url psycopg2-binary# blogsite/settings.py
import dj_database_url
DATABASES = {
"default": dj_database_url.config(default=f"sqlite:///{BASE_DIR / 'db.sqlite3'}")
}SQLite has been fine for every part of this series so far — a single file, zero setup. It's a poor fit for production because it can't handle concurrent writes from multiple server processes safely. dj_database_url.config() reads a DATABASE_URL environment variable (the format almost every host provides for a managed Postgres instance) and falls back to the local SQLite file when that variable isn't set, so nothing about local development changes.
The actual entry point
pip install gunicorngunicorn blogsite.wsgi:applicationrunserver is explicitly documented as unfit for production — it's single-threaded and has no protection against the kind of load a real server needs to handle. gunicorn is a production WSGI server; blogsite.wsgi:application points it at the application object wsgi.py has exported since part 1, unused until now.
Picking a host
Railway and Render both have straightforward free/low-cost tiers for exactly this shape — a Python web process plus a managed Postgres add-on — with SECRET_KEY, DEBUG, ALLOWED_HOSTS, and DATABASE_URL set in the platform's dashboard rather than committed to git.
After deploying, run python manage.py migrate against the production database (most hosts give you a one-off command runner for this) and create a superuser there too — the local SQLite database and its data never travel with the deploy.
What you built
Twenty-two parts back, this was an empty folder. It's now a deployed Django application with models and migrations, a full admin, class-based views for every CRUD operation, forms with real validation, authentication and per-user permissions, file uploads, pagination, and a test suite that actually catches regressions. That's not a toy project shape — it's the same one nearly every real Django app is built from, just with more features layered on top of exactly this foundation.