~/TechPurAI
~/tutorials/django-from-scratch/deploying-django
intermediate·part 22 of 22·3 min read

Deploying Django: production settings and going live

Updated Aug 15, 2026Python · Django

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

python
# 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

python
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

bash
pip install whitenoise
python
# 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

bash
pip install dj-database-url psycopg2-binary
python
# 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

bash
pip install gunicorn
bash
gunicorn blogsite.wsgi:application

runserver 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.

Checkpoint

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.

VK

Vijay Kumar

Founder of TechPurAI — writing hands-on tutorials and honest tool breakdowns.

LinkedIn ↗
← previous21. Testing a Django app: TestCase and the test client