Deployment: WSGI, Static & Settings
Reviewed & published by Brayan K
Deployment is the process of configuring and running your Django project on a production server safely, with debugging off, secrets in the environment, static files collected, and a real WSGI server handling requests.
Part of the free Django course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll harden settings with DEBUG=False, ALLOWED_HOSTS, and an environment-driven SECRET_KEY, collect static files with collectstatic and WhiteNoise, serve the app through gunicorn and WSGI, parse a database URL, and run the deploy checklist.
What You'll Learn in This Lesson
- Set DEBUG=False and ALLOWED_HOSTS
- Load SECRET_KEY and secrets from the environment
- Collect static files with collectstatic + WhiteNoise
- Serve the app with gunicorn through WSGI
- Parse a DATABASE_URL into the DATABASES setting
- Run manage.py check --deploy before going live
1 Production Settings from the Environment
The most important production change is turning DEBUG off and listing your real domains in ALLOWED_HOSTS. Secrets like SECRET_KEY must come from the environment, never from a file in your repository. You read them with os.environ at startup.
# settings.py
import os
# Secrets come from the environment — raise loudly if missing
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
# Env vars are strings, so compare against "True"
DEBUG = os.environ.get("DJANGO_DEBUG", "False") == "True"
# Comma-separated domains, split into a list
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "").split(",")# In production you read secrets from the environment, never hard-code them.
# os.environ.get returns None if missing, so we provide defaults and raise
# loudly when a REQUIRED setting like SECRET_KEY is absent.
def get_env(environ, key, default=None, required=False):
value = environ.get(key, default)
if required and value is None:
raise RuntimeError(f"Missing required env var: {key}")
return value
# Pretend this is the server's real environment
environ = {
"DJANGO_SECRET_KEY": "super-secret-from-vault",
"DJANGO_DEBUG": "False",
"DJANGO_ALLOWED_HOSTS": "example.com,www.example.com",
}
SECRET_KEY = get_env(environ, "DJANGO_SECRET_KEY", required=True)
DEBUG = get_env(environ, "DJANGO_DEBUG", "False") == "True"
ALLOWED_HOSTS = get_env(environ, "DJANGO_ALLOWED_HOSTS", "").split(",")
print("SECRET_KEY loaded:", SECRET_KEY[:6] + "...")
print("DEBUG:", DEBUG)
print("ALLOWED_HOSTS:", ALLOWED_HOSTS)
# EXPECTED:
# SECRET_KEY loaded: super-...
# DEBUG: False
# ALLOWED_HOSTS: ['example.com', 'www.example.com']
# ✅ Expected output:
# SECRET_KEY loaded: super-...
# DEBUG: False
# ALLOWED_HOSTS: ['example.com', 'www.example.com']2 Static Files, the Database URL, and gunicorn
In production the dev server doesn't serve static files, so you run collectstatic to gather them into STATIC_ROOT and let WhiteNoise serve them. Your database connection arrives as a single DATABASE_URL, and you run the app through gunicorn, which loads your project's WSGI callable.
# settings.py — static files served by WhiteNoise
STATIC_URL = "/static/"
STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles") # collectstatic target
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware", # right after security
# ... the rest ...
]# Build and run on the server
python manage.py collectstatic --noinput # gather static files
python manage.py migrate # apply migrations
# Start the WSGI server (project/wsgi.py exposes "application")
gunicorn project.wsgi:application --bind 0.0.0.0:8000 --workers 3# A database URL packs the whole connection into one string, e.g.
# postgres://user:pass@host:5432/dbname. We parse it into the dict Django's
# DATABASES setting expects — the same job dj-database-url does.
from urllib.parse import urlparse
def parse_database_url(url):
parts = urlparse(url)
engines = {
"postgres": "django.db.backends.postgresql",
"mysql": "django.db.backends.mysql",
"sqlite": "django.db.backends.sqlite3",
}
return {
"ENGINE": engines[parts.scheme],
"NAME": parts.path.lstrip("/"),
"USER": parts.username,
"PASSWORD": parts.password,
"HOST": parts.hostname,
"PORT": parts.port,
}
config = parse_database_url("postgres://app:s3cret@db.example.com:5432/shop")
for key in ["ENGINE", "NAME", "USER", "HOST", "PORT"]:
print(f"{key}: {config[key]}")
# EXPECTED:
# ENGINE: django.db.backends.postgresql
# NAME: shop
# USER: app
# HOST: db.example.com
# PORT: 5432
# ✅ Expected output:
# ENGINE: django.db.backends.postgresql
# NAME: shop
# USER: app
# HOST: db.example.com
# PORT: 54323 The Deploy Checklist (check --deploy)
Django ships a built-in safety audit: python manage.py check --deploy. It inspects your settings and warns about anything unsafe for production — DEBUG left on, an empty ALLOWED_HOSTS, a weak SECRET_KEY, or missing SSL settings. Run it before every release.
# Run the production audit
python manage.py check --deploy
# Example warnings it raises:
# W008 SECURE_SSL_REDIRECT should be True
# W009 SECRET_KEY has less than 50 characters / low entropy
# W018 DEBUG should not be True in deployment
# W020 ALLOWED_HOSTS must not be empty in deployment
# Recommended production security settings:
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000# manage.py check --deploy runs a checklist of production safety rules.
# Here we model the core checks: DEBUG must be off, ALLOWED_HOSTS must be set,
# SECRET_KEY must be strong, and SSL redirect should be on. Each failure is a
# warning, exactly like Django's deploy check output.
def deploy_check(settings):
warnings = []
if settings.get("DEBUG"):
warnings.append("W018: DEBUG should be False in production")
if not settings.get("ALLOWED_HOSTS"):
warnings.append("W020: ALLOWED_HOSTS must not be empty")
if len(settings.get("SECRET_KEY", "")) < 50:
warnings.append("W009: SECRET_KEY is too short / weak")
if not settings.get("SECURE_SSL_REDIRECT"):
warnings.append("W008: SECURE_SSL_REDIRECT should be True")
return warnings
bad = {"DEBUG": True, "ALLOWED_HOSTS": [], "SECRET_KEY": "x", "SECURE_SSL_REDIRECT": False}
good = {
"DEBUG": False,
"ALLOWED_HOSTS": ["example.com"],
"SECRET_KEY": "k" * 50,
"SECURE_SSL_REDIRECT": True,
}
print("BAD settings:")
for w in deploy_check(bad):
print(" " + w)
print("GOOD settings:", deploy_check(good) or "0 issues — ready to deploy")
# EXPECTED:
# BAD settings:
# W018: DEBUG should be False in production
# W020: ALLOWED_HOSTS must not be empty
# W009: SECRET_KEY is too short / weak
# W008: SECURE_SSL_REDIRECT should be True
# GOOD settings: 0 issues — ready to deploy
# ✅ Expected output:
# BAD settings:
# W018: DEBUG should be False in production
# W020: ALLOWED_HOSTS must not be empty
# W009: SECRET_KEY is too short / weak
# W008: SECURE_SSL_REDIRECT should be True
# GOOD settings: 0 issues — ready to deploy🎯 Your Turn: Read DEBUG from the Environment
Environment variables are strings, so "False" is truthy. Fill in the blank so DEBUG becomes the boolean False only when the variable is exactly "True".
# 🎯 YOUR TURN: DEBUG must be False in production. The env var arrives as the
# STRING "False", so a plain bool("False") would wrongly be True.
# Replace ___ so DEBUG becomes the boolean False.
raw = "False" # value from os.environ["DJANGO_DEBUG"]
DEBUG = raw == ___
print("DEBUG:", DEBUG)
print("type:", type(DEBUG).__name__)
# EXPECTED OUTPUT:
# DEBUG: False
# type: boolCommon Errors (And How to Fix Them)
❌ DisallowedHost / Bad Request (400) after deploy
With DEBUG=False, Django rejects any host not listed in ALLOWED_HOSTS.
✅ Fix: add your domain(s) to ALLOWED_HOSTS, e.g. ["example.com", "www.example.com"].
❌ CSS and JS 404 in production
You never collected static files, or nothing is serving them.
✅ Fix: run collectstatic and add WhiteNoise middleware (or a CDN) to serve STATIC_ROOT.
❌ Secret key committed to the repository
A hard-coded SECRET_KEY in settings.py ends up in version control.
✅ Fix: read it from os.environ and store the value in your platform's secret manager.
📋 Quick Reference
| Setting / Command | Why It Matters |
|---|---|
| DEBUG = False | Hide tracebacks; enforce ALLOWED_HOSTS |
| ALLOWED_HOSTS | Domains the site is allowed to serve |
| collectstatic | Gather static files into STATIC_ROOT |
| gunicorn project.wsgi | Run the app as a WSGI server |
| check --deploy | Audit settings for production safety |
🏆 Mini Challenge: A Deploy-Readiness Gate
Build a gate that loads settings from the environment, runs the production checklist, and only "deploys" when there are zero issues — otherwise it reports the blocking problems.
# 🏆 Mini Challenge: a deploy-readiness gate. Load settings from the
# environment, run the production checklist, and only "deploy" when there
# are zero issues. Otherwise print the blocking problems.
def load_settings(environ):
return {
"DEBUG": environ.get("DJANGO_DEBUG", "False") == "True",
"ALLOWED_HOSTS": [h for h in environ.get("DJANGO_ALLOWED_HOSTS", "").split(",") if h],
"SECRET_KEY": environ.get("DJANGO_SECRET_KEY", ""),
}
def readiness(settings):
issues = []
if settings["DEBUG"]:
issues.append("DEBUG is True")
if not settings["ALLOWED_HOSTS"]:
issues.append("ALLOWED_HOSTS is empty")
if len(settings["SECRET_KEY"]) < 50:
issues.append("SECRET_KEY is weak")
return issues
def deploy(environ):
settings = load_settings(environ)
issues = readiness(settings)
if issues:
return "BLOCKED: " + "; ".join(issues)
return "Deploying to production..."
dev_env = {"DJANGO_DEBUG": "True"}
prod_env = {
"DJANGO_DEBUG": "False",
"DJANGO_ALLOWED_HOSTS": "example.com",
"DJANGO_SECRET_KEY": "z" * 50,
}
print(deploy(dev_env))
print(deploy(prod_env))
# EXPECTED:
# BLOCKED: DEBUG is True; ALLOWED_HOSTS is empty; SECRET_KEY is weak
# Deploying to production...❓ Frequently Asked Questions
Lesson complete — you can ship to production!
You can harden settings with DEBUG=False, ALLOWED_HOSTS, and an env-driven SECRET_KEY, collect static files with collectstatic and WhiteNoise, run the app through gunicorn and WSGI, parse a database URL, and audit with check --deploy.
Practice quiz
What must DEBUG be set to in production?
- True
- False
- 1
- 'on'
Answer: False. DEBUG=False hides tracebacks, enforces ALLOWED_HOSTS, and serves a generic 500 page.
Why is DEBUG=True dangerous in production?
- It makes the site slower only
- It disables the database
- It exposes code, settings, and environment in error pages
- It blocks all requests
Answer: It exposes code, settings, and environment in error pages. The debug error page leaks code, settings, and env vars to anyone who triggers an error.
What does collectstatic do?
- Compiles your Python code
- Gathers static files into STATIC_ROOT
- Minifies JavaScript only
- Starts the web server
Answer: Gathers static files into STATIC_ROOT. collectstatic gathers static files from all apps into the single STATIC_ROOT directory.
What is WSGI?
- A database driver
- A template engine
- A caching backend
- The standard interface between a Python web app and a server
Answer: The standard interface between a Python web app and a server. WSGI is the standard interface; project/wsgi.py exposes the 'application' callable.
Which command starts a production WSGI server for project/wsgi.py?
- gunicorn project.wsgi:application
- python manage.py runserver
- django-admin serve
- wsgi start project
Answer: gunicorn project.wsgi:application. gunicorn imports the WSGI application callable and serves it with worker processes.
What does WhiteNoise let you do?
- Encrypt the database
- Serve collected static files efficiently from the app server
- Run background tasks
- Manage migrations
Answer: Serve collected static files efficiently from the app server. WhiteNoise serves the collected static files with caching headers, the simplest production option without a CDN.
Why must ALLOWED_HOSTS be set when DEBUG=False?
- It speeds up rendering
- It configures static files
- Django rejects any host not listed, raising DisallowedHost
- It enables the admin
Answer: Django rejects any host not listed, raising DisallowedHost. With DEBUG off Django enforces ALLOWED_HOSTS and returns 400 for unlisted hosts.
How should SECRET_KEY be supplied in production?
- Hard-coded in settings.py
- Committed to the repository
- Generated fresh on every request
- Read from an environment variable, never committed
Answer: Read from an environment variable, never committed. Read SECRET_KEY from os.environ and keep it out of source control.
Which command audits settings for production safety?
- python manage.py check --deploy
- python manage.py audit
- python manage.py secure
- python manage.py validate
Answer: python manage.py check --deploy. check --deploy warns about DEBUG, empty ALLOWED_HOSTS, weak SECRET_KEY, and missing SSL settings.
Why compare an env var explicitly with == 'True' for DEBUG?
- It is faster than bool()
- Env vars are strings, so even 'False' is truthy
- Django requires the string form
- It avoids importing os
Answer: Env vars are strings, so even 'False' is truthy. Environment values arrive as strings; any non-empty string is truthy, so compare to 'True'.
Continue this course
- Previous: DRF Authentication & Permissions
- Next: Checkpoint: Build a Django REST API — Combine pagination, auth, caching and serializers into a JSON API — then a quiz
- Quick reference: Django cheat sheet
Frequently asked questions
Why must DEBUG be False in production?
With DEBUG set to True, Django shows a detailed error page that exposes your code, settings, and environment variables whenever something goes wrong. That is invaluable while developing but a serious security leak in production, where it would hand attackers a map of your application. In production you set DEBUG to False, which also means Django enforces ALLOWED_HOSTS and serves a generic 500 page instead of the debug traceback.
What is WSGI and where does gunicorn fit in?
WSGI is the standard interface between a Python web application and a web server. Your project ships with a wsgi.py file that exposes an application callable. In production you do not use the development server; instead you run a WSGI server like gunicorn that imports that callable and serves it across multiple worker processes. A front web server such as nginx usually sits in front of gunicorn to handle TLS and static files.
What does collectstatic do and why do I need WhiteNoise?
collectstatic gathers every static file from your apps and third-party packages into the single STATIC_ROOT directory so one server can serve them all. In production the development server does not serve static files, so you need something that does. WhiteNoise is a small library that lets gunicorn serve those collected files efficiently with correct caching headers, which is the simplest production option when you are not using a separate CDN.
How should I manage SECRET_KEY and other secrets?
Never commit SECRET_KEY, database passwords, or API keys to source control. Read them from environment variables at startup using os.environ, and store the actual values in your hosting platform's secret manager or a local untracked .env file. This keeps secrets out of your repository, lets each environment use different values, and makes rotating a leaked key a configuration change rather than a code change.