Django is a mature framework, but production deployment still trips up teams who've only run manage.py runserver. In 2026, the path is well-understood: Gunicorn as the WSGI server, Whitenoise or a CDN for static files, a production settings module with no secrets, and migrations that run automatically before traffic arrives. This guide walks every step.
What changed in 2026
- Django 5.x (current LTS branch) ships with async views and async ORM as first-class features — relevant if you're adding async endpoints.
uv is the dominant package manager; uv sync --frozen in Docker is faster than pip by an order of magnitude.
- Whitenoise 7 supports Brotli compression and HTTP/2 push hints, making it a viable CDN replacement for many small-to-medium apps.
- Managed platforms (Railway, Fly.io, Render) handle TLS, zero-downtime deploys, and Postgres provisioning, dramatically reducing ops overhead for Django apps.
Settings split
myproject/
settings/
__init__.py # empty
base.py # shared settings
development.py
production.py
# settings/base.py
from pathlib import Path
import os
BASE_DIR = Path(__file__).resolve().parent.parent.parent
SECRET_KEY = os.environ["SECRET_KEY"] # never hardcode
DEBUG = False # override in development.py only
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "").split(",")
INSTALLED_APPS = [
"django.contrib.admin",
# ...
"whitenoise.runserver_nostatic",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
# ...
]
STATIC_ROOT = BASE_DIR / "staticfiles"
STATIC_URL = "/static/"
STORAGES = {
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
# settings/production.py
from .base import *
SECURE_HSTS_SECONDS = 31536000
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
Set DJANGO_SETTINGS_MODULE=myproject.settings.production via environment variable.
The Dockerfile
FROM python:3.13-slim AS builder
WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN pip install uv && uv sync --frozen --no-dev
FROM python:3.13-slim AS runtime
WORKDIR /app
RUN adduser --disabled-password --gecos "" django
COPY --from=builder /app/.venv /app/.venv
COPY . .
ENV PATH="/app/.venv/bin:$PATH"
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV DJANGO_SETTINGS_MODULE=myproject.settings.production
USER django
EXPOSE 8000
# Collect static, migrate, then start
CMD ["sh", "-c", "python manage.py collectstatic --noinput && python manage.py migrate && gunicorn myproject.wsgi:application --bind 0.0.0.0:8000 --workers 4 --timeout 120 --access-logfile -"]
For production, separate the migration step into an init container or a pre-deploy hook so it runs once, not on every worker startup.
Gunicorn configuration
# gunicorn.conf.py
workers = 4 # (2 × cores) + 1 is the heuristic
worker_class = "sync" # or "gthread" for threaded
timeout = 120
bind = "0.0.0.0:8000"
accesslog = "-"
errorlog = "-"
loglevel = "info"
Run with: gunicorn myproject.wsgi:application -c gunicorn.conf.py
Django security checklist
| Setting |
Production value |
DEBUG |
False |
SECRET_KEY |
Long random string, from env |
ALLOWED_HOSTS |
Exact domain list, from env |
SECURE_SSL_REDIRECT |
True |
SECURE_HSTS_SECONDS |
31536000 (1 year) |
SESSION_COOKIE_SECURE |
True |
CSRF_COOKIE_SECURE |
True |
Run python manage.py check --deploy — Django outputs all security warnings automatically.
Static files options
| Approach |
When to use |
| Whitenoise |
Simple apps; no CDN setup needed |
| S3 + CloudFront |
High-traffic; offload serving entirely |
| R2 + Cloudflare CDN |
S3-compatible, cheaper egress |
How to pick your deployment target
- Railway — drag-and-drop with a Dockerfile; provisions Postgres automatically. Fastest path to live.
- Fly.io — global PoPs, always-on, simple
fly deploy. Good for latency-sensitive apps.
- Render — static sites, web services, and cron jobs in one dashboard; first-class Django support.
- Heroku — more expensive in 2026 but still has the most Django tutorials and ecosystem.
- VPS (Hetzner, DigitalOcean) — cheapest for steady-state traffic; you manage Nginx, Certbot, and supervisor.
Common mistakes
Running runserver in production. It is not thread-safe, not multi-process, and runs in debug mode. Always Gunicorn.
Hardcoding SECRET_KEY in settings.py. It ends up in git, then in your history forever. Read from env on startup.
Not collecting static files before deploy. Django won't serve /static/ through Gunicorn without Whitenoise, and it will 404.
Blocking the deploy on a long migration. For tables with millions of rows, use --fake for schema-only changes and do data backfills separately.
Not setting ALLOWED_HOSTS. Django raises DisallowedHost for every request; production will 400 all traffic.
What to skip
mod_wsgi (Apache) — valid historically, but Gunicorn + Nginx is far simpler and faster for new deployments.
daphne unless you need channels — ASGI overhead is not worth it for a pure WSGI Django app.
- Django's built-in development server for anything real — even for load tests.
FAQ
Should I use WSGI or ASGI in 2026?
WSGI (Gunicorn) unless you need WebSockets or async views. Switching to ASGI (with Daphne or Uvicorn) adds complexity without benefit for standard request/response apps.
How do I run migrations safely?
Use an init container or a release phase command (Heroku, Railway support this). Run migrations before new app instances start to avoid schema mismatches.
How do I manage Django secrets in production?
AWS Secrets Manager, GCP Secret Manager, or Doppler — inject as environment variables at runtime. Never commit them.
How many Gunicorn workers for Django?
Start with (2 × CPU cores) + 1. Django views are typically I/O-bound (DB queries), so you can increase this, but watch RAM per worker (~50–150 MB each).
Where to go next
See How to write a database migration in 2026, How to set up Postgres locally in 2026, and How to add full-text search in 2026.