IT
Accedi Crea un account

Errori comuni nell'implementazione di Django in produzione e come evitarli

Bad Request (400), admin senza stili, verifica CSRF fallita? Ecco i 10 errori più comuni nell'implementazione di Django in produzione, come riconoscerli e risolverli, più una checklist.

Grzegorz Kalmus ·

L'applicazione Django funziona perfettamente sul tuo laptop, ma una volta caricata sul server mostra "Bad Request (400)", il pannello amministrativo senza stili oppure una pagina vuota con errore 500. Non è sfortuna, ma alcune trappole ricorrenti in cui quasi tutti cadono al primo deployment. L'ambiente di sviluppo Django è volutamente indulgente: mostra gli errori, serve autonomamente i file statici e non richiede informazioni sul dominio. La produzione, invece, non perdona nulla di tutto ciò.

In questa guida abbiamo raccolto i 10 errori più comuni durante il deployment di Django in produzione. Per ciascuno descriviamo come si manifesta, da dove nasce e come risolverlo. Alla fine trovi una breve checklist da controllare prima di ogni deployment e le risposte alle domande più frequenti.

Indice

  1. Perche Django funziona in locale ma non sul server?
  2. Errore 1: E un grosso problema avere DEBUG = True in produzione?
  3. Errore 2: Dove conservare SECRET_KEY e password?
  4. Errore 3: Da dove viene il "Bad Request (400)"?
  5. Errore 4: Perche dietro proxy appare "CSRF verification failed"?
  6. Errore 5: Perche il pannello admin non ha stili?
  7. Errore 6: Si puo lasciare runserver?
  8. Errore 7: Cosa succede se dimentichi le migrazioni?
  9. Errore 8: SQLite basta in produzione?
  10. Errore 9: Perche pip install rovina il deployment?
  11. Errore 10: Come sapere se l'applicazione e caduta?
  12. Lista di controllo prima del deployment
  13. Come funziona su Saplo?
  14. FAQ

Perché Django funziona in locale ma non sul server?

Il comando python manage.py runserver avvia il server di sviluppo, che fa silenziosamente un sacco di cose per lo sviluppatore. Fornisce i file CSS e JavaScript, in caso di errore mostra una pagina chiara con lo stack trace, accetta richieste su localhost e non si preoccupa di HTTPS.

In produzione questo comfort scompare. L'applicazione è dietro a un server web o a un reverse proxy, funziona sotto un vero dominio, il traffico arriva cifrato e ogni elemento che in locale "funzionava da solo" deve essere configurato esplicitamente. La maggior parte degli errori descritti qui sotto deriva proprio da questa differenza tra i due ambienti.

Prima di andare avanti, memorizza un comando. Django ha un audit integrato per le impostazioni di produzione:

python manage.py check --deploy

Mostra avvisi su DEBUG, chiavi, cookie, HTTPS e intestazioni di sicurezza. Eseguirlo prima della prima messa in produzione cattura metà dei problemi di questa lista.

Errore 1: DEBUG = True in produzione è un grosso problema?

Sì, lo è. Con DEBUG attivato, ogni errore 500 mostra al visitatore una pagina diagnostica completa: frammenti di codice, variabili locali, percorsi sul server, elenco delle impostazioni e query SQL. Django nasconde alcune denominazioni sensibili, ma non tutte, e la struttura stessa dell'applicazione rappresenta per un attaccante una mappa preziosa. Inoltre, la modalità DEBUG memorizza ogni richiesta al database, quindi con un utilizzo prolungato la memoria del processo cresce.

Come risolvere: imposta DEBUG tramite una variabile d'ambiente, con un valore predefinito sicuro.

import os
DEBUG = os.environ.get("DJANGO_DEBUG", "0") == "1"

Localmente imposti DJANGO_DEBUG=1, mentre sul server non imposti nulla. Ricorda solo che disattivando DEBUG entrano in vigore ALLOWED_HOSTS e Django smette di servire i file statici. Perciò, gli errori 3 e 5 in genere appaiono subito dopo aver risolto questo.

Errore 2: Dove conservare SECRET_KEY e password?

Non nel repository. SECRET_KEY firma sessioni, token per il reset della password e cookie CSRF. Chi lo conosce può falsificare la sessione di un utente autenticato. La chiave generata da startproject spesso finisce nel primo commit e rimane nella storia di git per sempre, anche dopo la sua rimozione dal file.

Lo stesso vale per la password del database, le chiavi API per i pagamenti o la posta.

Come risolvere: leggi tutti i segreti dalle variabili d’ambiente, lasciando nel repository solo i nomi.

SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]

L’assenza della variabile causerà un errore all’avvio, ed è giusto così: meglio che l’applicazione non si avvii, piuttosto che partire con una chiave predefinita. Se la chiave è già finita in un repository pubblico, genera una nuova. Tutti gli utenti verranno disconnessi, ma la chiave vecchia non sarà più pericolosa.

Errore 3: Cosa causa il “Bad Request (400)”?

Quasi sempre è dovuto a un ALLOWED_HOSTS vuoto o incompleto. Con DEBUG disattivato, Django rifiuta ogni richiesta il cui header Host non è presente in questa lista. Gli errori più comuni sono la mancanza della versione con www, l’assenza del sottodominio temporaneo sotto cui l’applicazione funzionava prima di collegare il dominio, oppure un errore di battitura.

Come risolvere: inserisci tutti i domini per cui l’applicazione deve rispondere.

ALLOWED_HOSTS = ["tuaazienda.it", "www.tuaazienda.it"]

Non risolvere il problema con l’inserimento di ["*"]. Questo disabilita la protezione contro attacchi sull’header Host, come ad esempio l’inserimento di domini falsi nei link per il reset della password.

Errore 4: Perché appare "CSRF verification failed" dietro un proxy?

In produzione Django raramente riceve traffico direttamente. Di fronte a lui c'è un reverse proxy che accetta HTTPS e inoltra alla applicazione normale HTTP. Django quindi vede la richiesta non criptata, mentre il browser invia il form dall'indirizzo https://. Dalla versione 4.0 Django confronta l'intestazione Origin con una lista di fonti fidate e senza di essa rifiuta i form, incluso il login al pannello admin.

Una seconda variante dello stesso problema: attivi SECURE_SSL_REDIRECT = True, ma la pagina finisce in un ciclo infinito di redirect perché Django pensa sempre che la richiesta sia arrivata via HTTP.

Come risolvere: indica a Django quale header il proxy usa per passare l'informazione HTTPS e aggiungi le fonti fidate.

SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
CSRF_TRUSTED_ORIGINS = ["https://tuaazienda.it", "https://www.tuaazienda.it"]
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True

L'impostazione SECURE_PROXY_SSL_HEADER è sicura solo se il proxy imposta sempre da sé questo header e non lascia passare valori dal cliente.

Errore 5: Perché il pannello admin non ha gli stili?

Quando si disattiva DEBUG, Django non serve i file statici. Il pannello /admin/ funziona, ma sembra una pagina degli anni '90, e i CSS personalizzati e le immagini restituiscono un 404. A questo si aggiunge un problema comune collectstatic: i file si trovano nelle cartelle delle app, ma nessuno li ha raccolti in un unico posto.

Come risolvere: imposta STATIC_ROOT, raccogli i file a ogni deploy e servili tramite il server web o la libreria WhiteNoise.

STATIC_ROOT = BASE_DIR / "staticfiles"
MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "whitenoise.middleware.WhiteNoiseMiddleware",
    # reszta middleware
]
python manage.py collectstatic --noinput

WhiteNoise gestisce solo i file statici provenienti dal codice. I file caricati dagli utenti (MEDIA_ROOT, ad esempio foto di prodotti) devono essere serviti separatamente, tramite server web o un archivio file esterno. Questo è un errore frequente subito dopo il primo.

Errore 6: Si può lasciare runserver?

No. La documentazione di Django è chiara: il server di sviluppo non ha superato audit di sicurezza né test di performance. Gestisce le richieste in modo non adatto al traffico reale, non ha una gestione sensata dei processi e, se si blocca, nessuno lo riavvierà.

Come risolvere: avvia l'applicazione tramite un server WSGI, solitamente Gunicorn, sotto il controllo di un process manager che lo riavvii in caso di crash o riavvio della macchina.

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

Il numero di processi worker dipende dalla memoria. Ogni processo di Gunicorn è una copia separata dell'applicazione in RAM, quindi su un piccolo server con 1 GB conviene iniziare con due e monitorare l'utilizzo, piuttosto che impostarne otto e incorrere in un killer di processi da parte del sistema.

Errore 7: Cosa succede se dimentichi le migrazioni?

La nuova versione del codice fa riferimento a una colonna che nel database ancora non esiste. La pagina che utilizza questo modello restituisce un errore 500 e nei log compare no such column oppure relation does not exist. Una variante di questo errore è il file di migrazione che non è stato caricato nel repository perché si trovava in .gitignore o nessuno l'ha aggiunto.

Come risolvere: considera le migrazioni come un passaggio fisso di ogni deployment, non come un’operazione manuale.

python manage.py makemigrations --check --dry-run
python manage.py migrate --noinput

Il primo comando, eseguito prima del commit, fallisce se nei modelli ci sono modifiche senza un file di migrazione. Il secondo viene eseguito sul server prima dell’avvio della nuova versione. Prima di una migrazione che elimina colonne o tabelle, fai una copia del database.

Errore 8: SQLite è sufficiente per la produzione?

Per una piccola applicazione con pochi utenti e poche scritture, SQLite può funzionare per anni. I problemi iniziano quando ci sono molte scritture simultanee (database is locked) e durante i backup: il file del database copiato durante una scrittura può risultare danneggiato.

Il vero errore non è scegliere SQLite, ma mantenere il database in un posto che scompare durante il deploy. Il file db.sqlite3 in una cartella sovrascritta a ogni deploy o in un container senza volume persistente significa che ogni nuovo deploy parte da un database vuoto.

Come risolvere: se l'applicazione deve crescere, passa a PostgreSQL e fornisci l'indirizzo del database nella variabile DATABASE_URL. Indipendentemente dal motore del database, assicurati che i dati risiedano in un posto persistente e siano coperti dal backup.

Errore 9: Perché pip install fa fallire il deployment?

Perché localmente funziona un ambiente diverso da quello del server. Scenari tipici: il file requirements.txt senza versione, quindi il server scarica una libreria più recente e incompatibile; un pacchetto installato manualmente in locale e non presente nel file (No module named ...); oppure una versione diversa di Python. Esempio reale: Django 6.0 richiede Python 3.12 o superiore, quindi su un server con Python 3.11 il comando pip install django senza limite di versione fallirà o installerà qualcosa che non funzionerà.

Come risolvere: assegna versioni specifiche e verifica quale versione di Python gira sul server.

pip freeze > requirements.txt
python --version

Se il server ha Python 3.11, nel file delle dipendenze scrivi Django>=5.2,<6. Il ramo 5.2 ha un supporto prolungato, quindi è una scelta ragionevole per la produzione.

Errore 10: Come capire se l'applicazione è caduta?

Lo scenario peggiore è quando a segnalare il malfunzionamento è il cliente. Con DEBUG disattivato, gli errori non sono visibili sul sito, quindi se non vengono registrati nei log o inviati via email, semplicemente scompaiono.

Come risolvere:

  • configura LOGGING in modo che gli errori vengano indirizzati all'output standard o a un file da controllare dopo il malfunzionamento,
  • imposta ADMINS e una corretta configurazione della posta, affinché Django invii una mail per ogni errore 500,
  • aggiungi un semplice indirizzo di controllo, per esempio /health/, che restituisce {"status": "ok"}, e collegalo al monitoraggio di disponibilità.

L'indirizzo di controllo sarà utile anche durante i deployment: il sistema di rilascio può verificare se la nuova versione è attiva prima di instradare il traffico su di essa.

Lista di controllo prima del rilascio

Prima di pubblicare una nuova versione, verifica questi punti:

Punto Come verificare
DEBUG disattivato DJANGO_DEBUG non è impostato sul server
Segreti fuori dal repository git grep SECRET_KEY non mostra i valori
Domini in ALLOWED_HOSTS versione con www e senza, più sottodominio temporaneo
HTTPS dietro proxy SECURE_PROXY_SSL_HEADER e CSRF_TRUSTED_ORIGINS impostati
File statici collectstatic nel processo di deploy, amministratore ha gli stili
Server applicazioni Gunicorn invece di runserver
Migrazioni makemigrations --check passa, migrate nel processo di deploy
Database e backup dati in posizione permanente, backup funzionante
Dipendenze versioni bloccate, compatibili con Python sul server
Log e monitoraggio errori finiscono nei log, /health/ risponde
Audit Django python manage.py check --deploy senza avvisi

Come funziona su Saplo?

Su Saplo gran parte di questa lista è già configurata prima ancora che tu scriva la prima riga di codice. Quando lanci un'applicazione Django dal catalogo, la procedura installa Gunicorn e WhiteNoise, configura STATIC_ROOT, inserisce il dominio in ALLOWED_HOSTS e CSRF_TRUSTED_ORIGINS, aggiunge SECURE_PROXY_SSL_HEADER, esegue le migrazioni e collectstatic, e il processo dell'applicazione è gestito da un process manager che lo riavvia in caso di crash. Il certificato SSL per il dominio è incluso nel prezzo del piano.

Nella distribuzione di un repository proprio i passi sono controllati dal file saplo.yaml. Le migrazioni si inseriscono come hook pre-build, la raccolta degli statici come comando di build:

build:
  install: "pip install -r requirements.txt"
  command: "python manage.py collectstatic --noinput"
hooks:
  pre_build: "python manage.py migrate --noinput"

Alcune cose da sapere:

  • Python 3.11. Il builder utilizza il Python 3.11 di sistema, quindi in requirements.txt mantieni Django sulla branch 5.x (errore 9).
  • Segreti. SECRET_KEY o la password del database la salvi con il comando saplo env set --secret o nel pannello, nella scheda Variabili e Segreti. Pannello, API e assistente AI mostreranno solo asterischi. La modifica è attiva dal prossimo deploy o riavvio. Dettagli nella documentazione di variabili e segreti.
  • Un deploy fallito non interrompe il sito. Se build o health check falliscono, la nuova versione ha stato di errore e la precedente continua a funzionare. L'health check attende risposte fino a 60 secondi.
  • PostgreSQL viene avviato come container nel file docker-compose.yml dell'app, con la porta esposta solo su 127.0.0.1 e l'indirizzo in variabile DATABASE_URL.
  • Backup. Ogni Box ha un backup giornaliero cifrato archiviato in un altro data center (7 giorni indietro su Box S, M e L). Dal Box L in poi sono disponibili snapshot automatici ripristinabili con un clic, e in ogni piano puoi fare uno snapshot manuale prima di una migrazione rischiosa.
  • Log si consultano con il comando saplo logs --follow o nel pannello, nei dettagli del deploy.

Il deploy da GitHub ad ogni git push è descritto nella guida Git deploy, e i messaggi d'errore più comuni li trovi nella risoluzione problemi.

Quale piano scegliere? Per una piccola app Django basta Box S (1 GB RAM, 27 € all'anno). Se ti serve accesso SSH al server o un database PostgreSQL in container separato, un buon inizio è Box M (2 GB RAM, SSH, 63 € all'anno). Box L (6 GB RAM, 136 € all'anno) aggiunge snapshot automatici degli ultimi 7 giorni. Il prezzo del primo anno è uguale al rinnovo, e la migrazione di un'app esistente da un altro hosting è gratuita. Il confronto completo dei piani è nel listino prezzi.

Se sei alle prime con Django, leggi prima cos'è Django e a cosa serve, e la guida per iniziare la trovi nel tutorial come far partire Django su Saplo.

FAQ

Come verificare se la mia applicazione Django è pronta per la produzione?

Esegui python manage.py check --deploy con le impostazioni di produzione. Il comando mostrerà avvisi riguardanti DEBUG, la chiave, HTTPS, i cookie e le intestazioni di sicurezza. Poi passa attraverso la checklist di questo articolo, perché l'audit non controlla migrazioni, backup o file statici.

Perché dopo aver disattivato DEBUG la pagina mostra Bad Request (400)?

Perché da quel momento Django verifica ALLOWED_HOSTS, e il dominio da cui accedi non è nella lista. Aggiungi tutte le varianti dell'indirizzo, anche con www e il sottodominio temporaneo, quindi riavvia l'applicazione.

WhiteNoise è sufficiente per i file statici in produzione?

Per la maggior parte delle applicazioni piccole e medie sì. WhiteNoise serve i file con compressione e un lungo tempo di caching nel browser. Tuttavia, non gestisce i file caricati dagli utenti, che devono essere serviti separatamente.

Quanta memoria RAM serve a Django in produzione?

Un'applicazione semplice con due processi Gunicorn occupa alcune centinaia di megabyte, quindi un server con 1 GB di RAM è sufficiente per cominciare. Serve più memoria se utilizzi un database PostgreSQL sullo stesso server, una coda di task in background o un numero maggiore di processi worker.

Posso trasferire un'applicazione Django funzionante su Saplo?

Sì. L'applicazione viene distribuita dal repository Git o tramite CLI, e il trasferimento da un altro hosting è gratuito su ogni piano. Prima della migrazione, è utile consultare la lista di controllo in questo articolo, poiché impostazioni dipendenti dal vecchio server, come domini o percorsi dei file, devono essere adattate al nuovo.

  • #Sicurezza
  • #Django
  • #Guida
  • #Python
  • #Deployment