Deploying to Production
Ship your app to the real internet — gunicorn + WSGI, DEBUG=False, ALLOWED_HOSTS, environment secrets, PostgreSQL and static files.
What you will learn
- Make a project production-safe (DEBUG, ALLOWED_HOSTS, secrets)
- Serve the app with gunicorn/WSGI and a real database
- Serve static files with WhiteNoise and go live
The dev server is not for production
The runserver command you have used is a development server — convenient, but slow and insecure, and the docs explicitly say never to use it for a live site. Going to production means swapping in production-grade pieces. Here is the cast of characters:
| Piece | Job |
|---|---|
| WSGI | the standard "socket" between Python web apps and servers |
| gunicorn | a fast production server that runs your Django app via WSGI |
| Nginx | a web server in front that handles incoming traffic / HTTPS |
| PostgreSQL | a real database (replaces the dev-only SQLite file) |
| WhiteNoise / S3 | serves your static files efficiently in production |
WSGI (Web Server Gateway Interface) is just an agreed-upon way for a Python web app and a web server to talk; Django created a wsgi.py file in your project for exactly this. gunicorn is the server that speaks WSGI and runs your code.
Step 1 — make settings production-safe
Three settings turn a development project into a safe one. Getting these wrong is the most common (and most dangerous) deployment mistake:
# settings.py
DEBUG = False # NEVER True in production
ALLOWED_HOSTS = ["yourdomain.com"] # only answer for hosts you own
# SECRET_KEY must come from the environment, not the code (next step)Watch out: With DEBUG = True on a live site, any error page shows your source code, settings and database details to the whole world. Set it to False before you deploy — this is the single biggest security mistake beginners make.
Why each matters:
DEBUG = False— hides the detailed error pages (which leak secrets) and shows a plain error instead.ALLOWED_HOSTS— Django refuses requests whoseHostheader is not in this list, blocking a class of attacks.- SECRET_KEY — signs sessions and tokens; if it leaks, attackers can forge them, so it must never sit in your committed code.
Step 2 — secrets in environment variables
An environment variable is a value the operating system holds outside your code, so you can keep secrets (the SECRET_KEY, the database password) out of Git. You read them in settings.py instead of hard-coding them:
# settings.py
import os
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"] # read from the environment
DEBUG = os.environ.get("DEBUG", "False") == "True"
# locally you keep these in a .env file (which is git-ignored):
# DJANGO_SECRET_KEY=super-long-random-string
# DEBUG=TrueNote: The rule: config that changes per environment, and anything secret, lives in environment variables — never in committed code. The .env file holding them must be listed in .gitignore.
Step 3 — a real database (PostgreSQL)
Development uses SQLite (a single file) — fine for learning, but production wants PostgreSQL, a robust multi-user database. You point Django at it in settings, again reading the connection details from the environment:
# settings.py
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["DB_HOST"],
"PORT": "5432",
}
}
# install the driver: pip install psycopg2-binaryAfter switching databases you run the same python manage.py migrate you already know — Django builds your tables in PostgreSQL exactly as it did in SQLite. Your models and queries do not change at all; that is the ORM paying off.
Step 4 — serve static files with WhiteNoise
With DEBUG = False, Django stops serving static files itself. WhiteNoise is a small tool that lets gunicorn serve your collected static files efficiently — no separate setup needed. Three lines wire it in:
# settings.py
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware", # add right after security
# ... the rest ...
]
STATIC_ROOT = BASE_DIR / "staticfiles"
STORAGES = {"staticfiles": {"BACKEND":
"whitenoise.storage.CompressedManifestStaticFilesStorage"}}
# install: pip install whitenoiseRemember the collectstatic command from the static-files lesson — you run it as part of deploying so WhiteNoise has the gathered files to serve.
Step 5 — run it with gunicorn
Locally you typed runserver. In production you start gunicorn instead, pointing it at your project’s WSGI application:
# install and run the production server
pip install gunicorn
gunicorn mysite.wsgi:application --bind 0.0.0.0:8000Note: Output: [INFO] Starting gunicorn 21.2.0 [INFO] Listening at: http://0.0.0.0:8000 [INFO] Booting worker with pid: 31 gunicorn is now serving your real Django app. In a full setup, Nginx sits in front to handle HTTPS and pass traffic to gunicorn.
The deployment checklist
Tie it all together. A reliable first deploy follows this order:
- Set
DEBUG = Falseand fill inALLOWED_HOSTSwith your domain. - Move
SECRET_KEY, database details and other secrets into environment variables. - Switch the database to PostgreSQL and run
python manage.py migrate. - Add WhiteNoise and run
python manage.py collectstatic. - Install and start gunicorn (
gunicorn mysite.wsgi:application). - Put it on a host (Render, Railway, Fly.io, a VPS with Nginx, or AWS) and visit your live URL.
Tip: Modern hosts like Render or Railway bundle gunicorn, PostgreSQL and environment variables into a few clicks, so your job is mostly the production-safe settings above. Whatever the host, the checklist is the same — and a live URL is the single most valuable thing you can show an employer.
Q. Which setting is the most dangerous to leave at its development value when you go live?
✍️ Practice
- Rewrite your
settings.pyto readSECRET_KEYandDEBUGfrom environment variables and setALLOWED_HOSTS. - Add WhiteNoise, run
collectstatic, and start your app locally withgunicorn mysite.wsgi:application.
🏠 Homework
- Deploy your project to a free host (Render or Railway) with DEBUG=False, PostgreSQL and static files working, and share the live URL.