Deploying Django

Django · Advanced · 14 min

Getting from runserver to a reliable production service involves a well-known checklist.

A production Django deployment usually looks like this:

  • App server: gunicorn mysite.wsgi (WSGI) or uvicorn mysite.asgi:application / gunicorn -k uvicorn_worker.UvicornWorker mysite.asgi (ASGI, for async views and WebSockets via Channels)
  • Reverse proxy or load balancer in front for TLS, buffering and serving static files
  • Database such as PostgreSQL, with persistent connections (CONN_MAX_AGE, plus CONN_HEALTH_CHECKS=True) or a pooler; Django 5.1+ also supports psycopg's native connection pool on PostgreSQL
  • Static files from collectstatic, served by WhiteNoise, the proxy or a CDN; media uploads on object storage via django-storages

Release steps, in order: build the image or artifact, python manage.py collectstatic --noinput, python manage.py migrate as a separate one-off step, then start new app processes.

Configuration checklist (python manage.py check --deploy covers much of it):

  • DEBUG = False, real SECRET_KEY, ALLOWED_HOSTS, CSRF_TRUSTED_ORIGINS for HTTPS origins if needed
  • HTTPS settings and SECURE_PROXY_SSL_HEADER behind a proxy
  • LOGGING to stdout in containers, plus error tracking (such as Sentry); ADMINS emails are an option on small sites
  • caches on Redis, sessions in the database or cache
  • a health check URL that touches the database

Write migrations to be backward compatible with the running code, since old and new versions overlap during a rolling deploy.

Example

# Dockerfile (excerpt)
FROM python:3.13-slim
ENV PYTHONUNBUFFERED=1 DJANGO_SETTINGS_MODULE=mysite.settings
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN DJANGO_SECRET_KEY=build-only python manage.py collectstatic --noinput
RUN useradd --create-home app
USER app
CMD ["gunicorn", "mysite.wsgi", "--bind", "0.0.0.0:8000", "--workers", "3", "--access-logfile", "-"]

# release step (run once per deploy, before switching traffic):
#   python manage.py migrate --noinput
# CI:
#   python manage.py check --deploy --fail-level WARNING

Remember

  • Gunicorn (WSGI) or Uvicorn (ASGI) behind a reverse proxy
  • collectstatic at build time, migrate as a separate release step
  • check --deploy, persistent DB connections, stdout logging
  • Keep migrations compatible with the previous code version

Watch out for

  • Running migrations automatically in every app process at startup, racing each other.

Check yourself

  1. Why run manage.py migrate as a separate release step rather than in each container's startup command?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.djangoproject.com/en/6.0/howto/deployment/checklist/

Builds on

Unlocks

Nothing builds on this yet.