Cos'è Nginx e perché va davanti a Django
Nginx è un web server e reverse proxy. Un reverse proxy riceve le richieste HTTP dai client e le inoltra al server applicativo — in questo caso Django — nascondendo l'applicazione dietro un unico punto di ingresso.
Django, tramite Gunicorn, è ottimo per elaborare la logica applicativa. Non è ottimo per servire file statici, gestire connessioni lente, terminare SSL, o fare rate limiting. Nginx fa tutte queste cose in modo molto più efficiente, liberando Gunicorn per quello che sa fare meglio.
La struttura in produzione è sempre la stessa:
Client → Nginx (porta 80/443) → Gunicorn (porta 8000) → DjangoNginx gestisce HTTPS, serve i file statici direttamente senza toccare Django, e protegge Gunicorn dall'esposizione diretta a internet. Gunicorn non deve mai essere raggiungibile dall'esterno — solo da Nginx, sulla rete interna del server.

Installazione e struttura dei file di configurazione
Su Ubuntu/Debian:
apt install nginx certbot python3-certbot-nginxI file di configurazione Nginx vivono in /etc/nginx/. La struttura che uso in produzione:
/etc/nginx/
nginx.conf ← configurazione globale
sites-available/
ekulos.com ← configurazione del sito
sites-enabled/
ekulos.com ← symlink a sites-availableAbilita il sito con:
ln -s /etc/nginx/sites-available/ekulos.com /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginxnginx -t verifica la sintassi prima di ricaricare — usalo sempre prima di un reload in produzione.
Configurazione base: reverse proxy verso Gunicorn
Questo è il blocco server minimo funzionante per Django + Gunicorn:
upstream django {
server 127.0.0.1:8000;
}
server {
listen 80;
server_name ekulos.com www.ekulos.com;
location / {
proxy_pass http://django;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}proxy_set_header X-Forwarded-For passa l'IP reale del client a Django — necessario per il logging e per il rate limiting applicativo. In Django, aggiungi SECURE_PROXY_SSL_HEADER e configura USE_X_FORWARDED_HOST = True per riconoscere i proxy header.
Servire i file statici e media direttamente
Django non deve mai servire file statici in produzione — è lento e spreca worker Gunicorn. Nginx li serve direttamente dal filesystem, molto più velocemente:
location /static/ {
alias /var/www/ekulos/staticfiles/;
expires 1y;
add_header Cache-Control "public, immutable";
}
location /media/ {
alias /var/www/ekulos/media/;
expires 30d;
add_header Cache-Control "public";
}expires 1y con immutable per i file statici — Django aggiunge un hash al nome del file ad ogni deploy, quindi puoi cachare aggressivamente. expires 30d per i media caricati dagli utenti, più conservativo perché il nome file non cambia.
In settings.py:
STATIC_ROOT = "/var/www/ekulos/staticfiles/"
MEDIA_ROOT = "/var/www/ekulos/media/"Dopo ogni deploy, esegui python manage.py collectstatic per copiare i file statici nella directory che Nginx serve.
HTTPS con Let's Encrypt
Certbot configura automaticamente HTTPS e aggiorna la configurazione Nginx:
certbot --nginx -d ekulos.com -d www.ekulos.comCertbot chiede email, accetta i termini, e modifica automaticamente la configurazione Nginx per aggiungere il blocco SSL. Il rinnovo automatico è già configurato via systemd timer — verifica che funzioni con:
certbot renew --dry-runDopo Certbot, la configurazione include automaticamente i parametri SSL raccomandati. Verifica con SSL Labs che il certificato sia configurato correttamente e che il rating sia almeno A.
Header di sicurezza
Gli header HTTP di sicurezza istruiscono il browser su come comportarsi — proteggono da XSS, clickjacking, e altri attacchi client-side. Aggiungili al blocco server:
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;X-Frame-Options: SAMEORIGIN — impedisce che la tua pagina venga caricata in un iframe su altri domini (protezione clickjacking).
X-Content-Type-Options: nosniff — impedisce al browser di interpretare i file con un tipo MIME diverso da quello dichiarato.
Strict-Transport-Security — forza HTTPS per un anno. Aggiungi solo dopo aver verificato che HTTPS funzioni correttamente — una volta impostato, il browser non accetterà più HTTP per quel dominio.
Content Security Policy è l'header più potente ma anche il più complesso da configurare — richiede di elencare esplicitamente le fonti consentite per script, stili, font, e immagini. Per Django con template server-side è relativamente semplice; per Next.js con molte dipendenze esterne richiede più lavoro. Vale la pena configurarlo, ma in una fase successiva.
Rate limiting
Nginx può limitare il numero di richieste per IP — protezione fondamentale contro brute force sul login e DDoS applicativo:
limit_req_zone $binary_remote_addr zone=generale:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;Il primo definisce una zona per il traffico generale (10 richieste/secondo per IP), il secondo per il login (5 richieste/minuto per IP — molto più restrittivo).
Applica le zone nelle location:
location / {
limit_req zone=generale burst=20 nodelay;
proxy_pass http://django;
}
location /accounts/login/ {
limit_req zone=login burst=3 nodelay;
proxy_pass http://django;
}burst permette un picco temporaneo oltre il rate — utile per navigazione normale che genera più richieste in rapida successione. nodelay risponde subito con 429 invece di mettere in coda le richieste in eccesso.
Attenzione ai file statici: assicurati che location /static/ non abbia rate limiting — Nginx li serve direttamente senza toccare Django, e un browser carica decine di asset in parallelo.
Timeout e buffer
I timeout di default di Nginx sono spesso troppo generosi per un'applicazione Django. Configura:
proxy_connect_timeout 10s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
client_max_body_size 10m;proxy_read_timeout è il tempo massimo che Nginx aspetta una risposta da Gunicorn. Se hai endpoint che fanno operazioni lunghe (generazione PDF, export CSV pesanti), alza solo per quelle location specifiche — non globalmente.
client_max_body_size limita la dimensione dei file uploadati. Il default di Nginx è 1MB — per la maggior parte dei form Django è troppo poco.
Checklist finale prima del go-live
Prima di puntare il DNS al nuovo server, verifica:
- nginx -t non riporta errori
- HTTPS funziona e il certificato è valido (certbot renew --dry-run passa)
- I file statici sono serviti da Nginx (controlla i log — non devono passare per Gunicorn)
- Gli header di sicurezza sono presenti (verifica con securityheaders.com)
- Il rate limiting è attivo sul path di login
- Gunicorn non è esposto direttamente su nessuna porta pubblica
- DEBUG = False in Django settings
- ALLOWED_HOSTS contiene solo il dominio reale
Un comando utile per verificare gli header in un colpo solo:
curl -I https://tuodominio.comMostra tutti gli header HTTP della risposta — puoi verificare immediatamente che X-Frame-Options, HSTS, e gli altri siano presenti.