Nginx + Gunicorn for Django: Why You Need Both for Production Deployment


4 views

When deploying Django in production, you'll often see Nginx paired with Gunicorn (or uWSGI). Here's why this combo works:


Client → Nginx (reverse proxy) → Gunicorn (app server) → Django

Nginx excels at:

  • Handling HTTP/HTTPS termination
  • Serving static files directly (CSS/JS/images)
  • Load balancing between multiple Gunicorn workers
  • Buffer slow clients (prevent worker blocking)
  • Handle SSL/TLS encryption

Gunicorn specializes in:

  • Running Python application code
  • Process management (workers/threads)
  • WSGI protocol implementation
  • Application lifecycle management

Here's a minimal Nginx config snippet:


server {
    listen 80;
    server_name example.com;

    location /static/ {
        alias /path/to/your/static/files;
    }

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

And Gunicorn startup command:


gunicorn --workers 3 --bind 127.0.0.1:8000 yourproject.wsgi:application

The async nature of Nginx complements Gunicorn's synchronous workers. While Gunicorn can technically serve HTTP directly:

  • Nginx handles ~10,000 static file requests/sec
  • Gunicorn typically manages 100-1000 dynamic requests/sec
  • Without Nginx, Gunicorn workers get bogged down serving static assets

This architecture provides defense in depth:


Internet → [Nginx (hardened)] → [Gunicorn (app layer)] → [Django]

Nginx can:

  • Filter malicious requests before they reach Django
  • Limit request sizes
  • Implement rate limiting
  • Add WAF rules

While possible to run just Gunicorn (for development) or Nginx with uWSGI, the Nginx+Gunicorn combo offers:

  • Easier configuration than uWSGI
  • Better performance for mixed static/dynamic content
  • More deployment flexibility

For high-traffic sites, you might later add:


CDN → Load Balancer → Multiple Nginx → Multiple Gunicorn

When deploying Django applications, Nginx and Gunicorn serve complementary but distinct roles. Gunicorn is a WSGI HTTP server that runs your Python application (like Django), while Nginx acts as a reverse proxy and web server. Together, they form a robust deployment stack.

While Gunicorn can directly serve HTTP requests, it's not optimized for:

  • Handling static files efficiently
  • Managing high traffic loads
  • SSL termination
  • Protection against certain web attacks

Here's the typical request flow:

Client → Nginx (reverse proxy) → Gunicorn (WSGI server) → Django

Here's a minimal Nginx configuration to proxy requests to Gunicorn:

server {
    listen 80;
    server_name yourdomain.com;

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    location /static/ {
        alias /path/to/your/static/files/;
    }
}

To run Gunicorn with your Django application:

gunicorn --workers 3 --bind 127.0.0.1:8000 yourproject.wsgi:application

The number of Gunicorn workers should generally be:

(2 x $num_cores) + 1

For a 4-core server, you'd use 9 workers.

Nginx serves static files much more efficiently than Django or Gunicorn. The configuration shown earlier handles this with the /static/ location block.

For a complete production setup, you'll want to:

  1. Set up process management (systemd or supervisor)
  2. Configure HTTPS with Let's Encrypt
  3. Implement proper logging
  4. Set up monitoring

While Nginx+Gunicorn is a popular choice, other options include:

  • uWSGI instead of Gunicorn
  • Traefik instead of Nginx
  • Daphne for async applications