Blog
3 min read

How to Deploy a Django App to Production (Step by Step)

Taking a Django project from runserver to production: settings for DEBUG, ALLOWED_HOSTS and secrets, Postgres, collectstatic and WhiteNoise, Gunicorn behind Nginx or Caddy, systemd, migrations on deploy, HTTPS, and the checklist Django itself gives you.

python manage.py runserver is for development only — Django's own docs say not to use it in production. Deploying properly means a few changes to your settings and a couple of extra pieces around your app. (What is Django?)

The production setup

Browser → Nginx or Caddy (HTTPS, static files) → Gunicorn → Django → PostgreSQL
  • Gunicorn — a production server that runs your Django app with several worker processes.
  • Nginx or Caddy — a reverse proxy that handles HTTPS and passes requests to Gunicorn. (Reverse proxies explained)
  • PostgreSQL — instead of the SQLite file used in development. (What is PostgreSQL?)

Step 1: production settings

Never deploy with development settings. Read sensitive values from environment variables:

# settings.py
import os

DEBUG = os.environ.get("DJANGO_DEBUG") == "1"
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "").split(",")
CSRF_TRUSTED_ORIGINS = ["https://example.com"]

SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
  • DEBUG = False in production. Debug pages show your code, settings and environment to anyone who triggers an error.
  • SECRET_KEY must be secret and not the one committed to Git. (Environment variables explained)
  • ALLOWED_HOSTS must list your domain(s), or Django returns 400 Bad Request.

Step 2: PostgreSQL

Install psycopg and configure DATABASES from a URL (the dj-database-url package makes this one line):

import dj_database_url
DATABASES = {"default": dj_database_url.config(conn_max_age=600)}

Set DATABASE_URL in the environment. (Postgres connection strings)

Step 3: static files

In production Django doesn't serve CSS and JS by itself. Collect them into one folder:

STATIC_ROOT = BASE_DIR / "staticfiles"
python manage.py collectstatic --noinput

Then either let Nginx/Caddy serve that folder, or add WhiteNoise so Gunicorn can serve them efficiently with no extra config. User uploads (MEDIA_ROOT) are different — store them somewhere persistent, ideally object storage. (Where should user uploads go?)

Step 4: dependencies

pip freeze > requirements.txt

Include django, gunicorn, psycopg[binary], dj-database-url, whitenoise. (requirements.txt explained)

Step 5: on the server

git clone https://github.com/you/mysite.git /srv/mysite
cd /srv/mysite
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python manage.py migrate
python manage.py collectstatic --noinput

(Python virtual environments)

Step 6: run Gunicorn as a service

# /etc/systemd/system/mysite.service
[Unit]
Description=mysite Django
After=network.target

[Service]
User=app
WorkingDirectory=/srv/mysite
EnvironmentFile=/srv/mysite/.env
ExecStart=/srv/mysite/.venv/bin/gunicorn mysite.wsgi:application --bind 127.0.0.1:8000 --workers 3
Restart=always

[Install]
WantedBy=multi-user.target
sudo systemctl enable --now mysite

(What is systemd?)

Step 7: HTTPS with a reverse proxy

With Caddy, the whole config is:

example.com {
    reverse_proxy 127.0.0.1:8000
}

(What is Caddy?) With Nginx, proxy to 127.0.0.1:8000 and get a certificate with Certbot. (Certbot and Let's Encrypt)

Step 8: Django's own checklist

python manage.py check --deploy

It warns about insecure settings — missing HSTS, cookies not marked secure, and more. Fix what applies.

On every deploy

git pull
pip install -r requirements.txt
python manage.py migrate
python manage.py collectstatic --noinput
sudo systemctl restart mysite

Automate it with a script or CI. (Deploy to a VPS with GitHub Actions)

Don't forget


EasySpawn runs Django the production way for you — Gunicorn behind HTTPS, Postgres with daily backups, migrations on deploy — and Claude Code can fix whatever check --deploy flags. See how it works or join the waitlist.

Related: What Is Django? · Flask vs Django vs FastAPI · How to Deploy a Flask App · How to Secure a New VPS

Keep reading