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:
- Set up process management (systemd or supervisor)
- Configure HTTPS with Let's Encrypt
- Implement proper logging
- 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