100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Python

Deploying a Django App

Walk through the practical steps of taking a Django project from `runserver` to a production-ready deployment with gunicorn, nginx, and a managed database.

Testing & DeploymentIntermediate10 min readJul 10, 2026
Analogies

Deploying a Django App

Django's built-in runserver is explicitly documented as unsuitable for production — it's single-threaded, has no process management, and serves static files inefficiently. A production deployment instead runs your application through a WSGI server like Gunicorn (or an ASGI server like Uvicorn/Daphne if you use async views or Channels), which Gunicorn workers handle concurrently, fronted by a reverse proxy like nginx that terminates TLS, serves static/media files directly, and forwards dynamic requests to Gunicorn over a Unix socket or local port.

🏏

Cricket analogy: Using runserver in production is like fielding a club-level net bowler in a World Cup final — fine for practice, but you need international-standard 'workers' (gunicorn processes) to handle real match pressure.

WSGI Servers and Static Files

Gunicorn is invoked with something like gunicorn myproject.wsgi:application --workers 3 --bind 0.0.0.0:8000, and the worker count is typically set to roughly (2 x CPU cores) + 1. Django itself does not serve static files (CSS, JS, images) efficiently or at all with DEBUG=False, so production deployments run python manage.py collectstatic to gather every app's static assets into STATIC_ROOT, then either let nginx serve that directory directly or use WhiteNoise middleware to serve compressed, cache-busted static files straight from the Django process itself, which is simpler when you don't want to manage a separate nginx static-file config.

🏏

Cricket analogy: collectstatic is like a groundsman gathering every piece of equipment scattered across different team dressing rooms into one central kit room before the tournament starts, so it's all in one place for match day.

dockerfile
FROM python:3.12-slim

WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .
RUN python manage.py collectstatic --noinput

CMD ["gunicorn", "myproject.wsgi:application", \
     "--workers", "3", "--bind", "0.0.0.0:8000"]

Database and Migrations in Production

Production deployments almost always use a managed database service (Amazon RDS, DigitalOcean Managed Postgres, or a self-hosted Postgres container with backups) rather than SQLite, which is fine for development but doesn't handle concurrent writes well. During deployment, python manage.py migrate must run against the production database before the new application code starts serving traffic — most teams run this as an explicit release step (a Kubernetes init container, a pre-deploy hook, or a CI/CD pipeline stage) rather than automatically on every container boot, to avoid multiple app instances racing to apply the same migration simultaneously.

🏏

Cricket analogy: Running migrations before the new app version goes live is like the ground staff finishing pitch preparation before the umpires call 'play' — you don't want batsmen (requests) arriving at a pitch (schema) that's still being relaid.

A common release pipeline order is: build the new image, run migrate as a one-off job against production, then perform a rolling deploy of app instances pointing at the now-migrated schema. This avoids old and new code running simultaneously against mismatched schema versions for long.

Containerizing with Docker

A typical production Docker setup uses a Dockerfile that installs dependencies, runs collectstatic, and starts Gunicorn as the container's entrypoint, orchestrated alongside a Postgres container and an nginx container via docker-compose.yml (or Kubernetes manifests for larger deployments). nginx is configured as a reverse proxy that terminates TLS certificates (often via Let's Encrypt/Certbot), serves the STATIC_ROOT volume directly for speed, and proxies everything else to the Gunicorn container's internal port, which keeps Django itself completely unreachable from the public internet.

🏏

Cricket analogy: nginx acting as the public-facing reverse proxy is like a team's media manager who handles all press and public interaction, while the players (Django/gunicorn) stay in the dressing room, never directly exposed to reporters.

Never expose Django's development server or the Gunicorn port directly to the public internet without a reverse proxy, and always confirm DEBUG=False, a proper ALLOWED_HOSTS list, and HTTPS-only cookie settings are active before pointing DNS at a production deployment.

  • runserver is unsuitable for production; use Gunicorn (WSGI) or Uvicorn/Daphne (ASGI) fronted by nginx as a reverse proxy.
  • Gunicorn worker count is typically (2 x CPU cores) + 1, tuned based on load testing.
  • python manage.py collectstatic gathers static assets into STATIC_ROOT for nginx or WhiteNoise to serve.
  • Production databases should be managed Postgres/MySQL services, not SQLite, which struggles with concurrent writes.
  • Run migrate as an explicit release step before new app instances start serving traffic, avoiding concurrent migration races.
  • nginx terminates TLS, serves static files directly, and reverse-proxies dynamic requests to Gunicorn, keeping Django off the public internet directly.
  • Always verify DEBUG=False, ALLOWED_HOSTS, and secure cookie settings before going live.

Practice what you learned

Was this page helpful?

Topics covered

#Python#DjangoStudyNotes#WebDevelopment#DeployingADjangoApp#Deploying#Django#App#WSGI#StudyNotes#SkillVeris