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) → Django

Nginx 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.

Django + Nginx in produzione: configurazione sicura passo dopo passo

Installazione e struttura dei file di configurazione

Su Ubuntu/Debian:

apt install nginx certbot python3-certbot-nginx

I 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-available

Abilita il sito con:

ln -s /etc/nginx/sites-available/ekulos.com /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx

nginx -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.com

Certbot 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-run

Dopo 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.com

Mostra tutti gli header HTTP della risposta — puoi verificare immediatamente che X-Frame-Options, HSTS, e gli altri siano presenti.