Servire un'applicazione Python con nginx
Distribuire un'applicazione Python in produzione richiede una architettura a più livelli: un application server che esegue il codice Python e un reverse proxy come nginx che gestisce le connessioni in ingresso, i file statici e la terminazione TLS. Questa guida mostra come collegare i due componenti in modo affidabile, partendo da un progetto Flask di esempio fino ad arrivare a una configurazione pronta per la produzione con Gunicorn e nginx su un server Linux.
Architettura generale
Il browser del client non parla mai direttamente con il processo Python. Il flusso di una richiesta è il seguente:
- Il client invia una richiesta HTTP/HTTPS a nginx (porta 80 o 443).
- nginx agisce da reverse proxy e inoltra la richiesta a Gunicorn tramite un socket Unix (o TCP).
- Gunicorn gestisce il pool di worker Python e consegna la richiesta all'applicazione WSGI.
- L'applicazione elabora la richiesta e restituisce una risposta che risale la catena.
Usare un socket Unix invece di un socket TCP elimina l'overhead di rete locale e migliora le prestazioni quando nginx e Gunicorn si trovano sullo stesso host.
Prerequisiti
Si assume un server con Ubuntu 22.04 LTS, accesso root o sudo, Python 3.10+ e nginx già installato. Se nginx non è presente, installarlo con:
# Aggiorna l'indice dei pacchetti e installa nginx
sudo apt update
sudo apt install -y nginx
Creare l'applicazione Python
Come esempio si usa Flask, ma la procedura è identica per Django, FastAPI (con Uvicorn al posto di Gunicorn) o qualsiasi altro framework WSGI/ASGI.
Struttura del progetto
/var/www/myapp/
├── app/
│ ├── __init__.py
│ └── views.py
├── wsgi.py
├── requirements.txt
└── .venv/
Creare l'ambiente virtuale
# Crea la cartella del progetto e l'ambiente virtuale
sudo mkdir -p /var/www/myapp
cd /var/www/myapp
python3 -m venv .venv
# Attiva l'ambiente virtuale
source .venv/bin/activate
# Installa le dipendenze
pip install flask gunicorn
Codice dell'applicazione
File app/__init__.py:
from flask import Flask
# Crea l'istanza principale dell'applicazione Flask
app = Flask(__name__)
# Importa le view dopo aver creato l'app per evitare importazioni circolari
from app import views # noqa: E402, F401
File app/views.py:
from app import app
@app.route("/")
def index():
# Risposta di benvenuto sulla rotta radice
return "Hello from Flask behind nginx!", 200
@app.route("/health")
def health_check():
# Endpoint usato da nginx e dai sistemi di monitoraggio per verificare lo stato
return {"status": "ok"}, 200
File wsgi.py:
from app import app
# Punto di ingresso WSGI usato da Gunicorn
if __name__ == "__main__":
app.run()
Configurare Gunicorn
Gunicorn è un server WSGI HTTP per Python che gestisce i worker e il ciclo di vita dei processi. Il numero ottimale di worker per una macchina con N core CPU è generalmente 2*N + 1.
Test manuale
# Avvia Gunicorn in primo piano per verificare che l'app funzioni
cd /var/www/myapp
source .venv/bin/activate
gunicorn --workers 3 --bind unix:/run/myapp.sock wsgi:app
Se non ci sono errori, Gunicorn è in ascolto sul socket Unix /run/myapp.sock. Interrompi il processo con Ctrl+C prima di procedere.
Creare un file di configurazione per Gunicorn
Anziché passare ogni argomento da riga di comando, si usa un file gunicorn.conf.py:
# /var/www/myapp/gunicorn.conf.py
# Indirizzo su cui Gunicorn ascolta: socket Unix per comunicare con nginx
bind = "unix:/run/myapp.sock"
# Numero di processi worker paralleli
workers = 3
# Classe worker: sync è adatta per applicazioni WSGI standard
worker_class = "sync"
# Timeout in secondi prima che un worker venga riavviato
timeout = 30
# File di log per gli accessi (usa "-" per stdout)
accesslog = "/var/log/myapp/access.log"
# File di log per gli errori
errorlog = "/var/log/myapp/error.log"
# Livello di dettaglio dei log
loglevel = "info"
# Utente e gruppo con cui girano i worker
user = "www-data"
group = "www-data"
# Crea la cartella per i log e assegna i permessi
sudo mkdir -p /var/log/myapp
sudo chown www-data:www-data /var/log/myapp
Creare un'unità systemd
Per avviare Gunicorn automaticamente al boot e gestirlo con i comandi systemctl, si crea un'unità systemd.
# /etc/systemd/system/myapp.service
[Unit]
Description=Gunicorn - server WSGI per myapp
# Si avvia dopo che la rete e nginx sono disponibili
After=network.target
[Service]
# Utente e gruppo non privilegiati per la sicurezza
User=www-data
Group=www-data
# Cartella di lavoro del processo
WorkingDirectory=/var/www/myapp
# Comando di avvio con il percorso completo all'eseguibile nell'ambiente virtuale
ExecStart=/var/www/myapp/.venv/bin/gunicorn \
--config /var/www/myapp/gunicorn.conf.py \
wsgi:app
# Riavvio automatico in caso di errore
Restart=on-failure
RestartSec=5s
[Install]
# Il servizio viene abilitato per il target multi-utente standard
WantedBy=multi-user.target
# Ricarica la configurazione di systemd
sudo systemctl daemon-reload
# Abilita il servizio all'avvio e avvialo subito
sudo systemctl enable --now myapp
# Verifica che sia in esecuzione
sudo systemctl status myapp
Configurare nginx come reverse proxy
nginx gestisce le connessioni in entrata, serve i file statici direttamente (senza coinvolgere Python) e inoltra le richieste dinamiche a Gunicorn.
File di configurazione del sito
Creare il file /etc/nginx/sites-available/myapp:
# /etc/nginx/sites-available/myapp
# Upstream che punta al socket Unix di Gunicorn
upstream gunicorn_server {
server unix:/run/myapp.sock fail_timeout=0;
}
server {
# Ascolta sulla porta 80 per le richieste HTTP
listen 80;
listen [::]:80;
# Sostituire con il proprio dominio o indirizzo IP
server_name example.com www.example.com;
# Dimensione massima del corpo della richiesta (upload)
client_max_body_size 10M;
# Timeout per la connessione con Gunicorn
proxy_read_timeout 90s;
# File statici serviti direttamente da nginx senza passare per Python
location /static/ {
alias /var/www/myapp/app/static/;
expires 30d;
add_header Cache-Control "public, no-transform";
}
# Favicon servita direttamente
location = /favicon.ico {
alias /var/www/myapp/app/static/favicon.ico;
log_not_found off;
access_log off;
}
# Tutto il resto viene inoltrato a Gunicorn
location / {
proxy_pass http://gunicorn_server;
# Intestazioni necessarie affinché Flask conosca il client reale
proxy_set_header Host $http_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;
# Disabilita il buffering per le risposte in streaming
proxy_buffering off;
}
}
Abilitare il sito e ricaricare nginx
# Crea il collegamento simbolico nella cartella sites-enabled
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
# Verifica la sintassi della configurazione di nginx
sudo nginx -t
# Ricarica nginx per applicare le modifiche
sudo systemctl reload nginx
Abilitare HTTPS con Let's Encrypt
In produzione, il traffico deve essere cifrato con TLS. Certbot automatizza l'ottenimento e il rinnovo dei certificati Let's Encrypt e modifica la configurazione di nginx in modo autonomo.
# Installa Certbot e il plugin per nginx
sudo apt install -y certbot python3-certbot-nginx
# Ottieni un certificato e configura nginx automaticamente
sudo certbot --nginx -d example.com -d www.example.com
# Verifica il rinnovo automatico (Certbot installa già un timer systemd)
sudo systemctl status certbot.timer
Dopo l'esecuzione di Certbot, la configurazione di nginx viene aggiornata con le direttive ssl_certificate, ssl_certificate_key e un redirect automatico da HTTP a HTTPS.
Gestione dei file statici con WhiteNoise (opzionale)
Se si preferisce che l'applicazione Python serva autonomamente i file statici senza delegare a nginx, si può usare il middleware WhiteNoise. Questo approccio è utile in ambienti containerizzati dove nginx non è presente, ma in produzione la soluzione con nginx rimane preferibile per le prestazioni.
pip install whitenoise
# wsgi.py - versione con WhiteNoise
import os
from whitenoise import WhiteNoise
from app import app
# Cartella dei file statici da servire con WhiteNoise
static_folder = os.path.join(os.path.dirname(__file__), "app", "static")
# Avvolge l'applicazione Flask con il middleware WhiteNoise
application = WhiteNoise(app, root=static_folder, prefix="static")
if __name__ == "__main__":
app.run()
Variabili d'ambiente e configurazione sicura
Non inserire mai segreti (chiavi API, password del database, SECRET_KEY) direttamente nel codice. Usare variabili d'ambiente caricate da un file .env con la libreria python-dotenv.
pip install python-dotenv
# app/__init__.py - caricamento sicuro della configurazione
import os
from flask import Flask
from dotenv import load_dotenv
# Carica le variabili dal file .env nella cartella radice del progetto
load_dotenv()
app = Flask(__name__)
# La chiave segreta viene letta dall'ambiente, mai dal codice sorgente
app.config["SECRET_KEY"] = os.environ["SECRET_KEY"]
from app import views # noqa: E402, F401
Nel file /etc/systemd/system/myapp.service, si può specificare un file di ambiente:
[Service]
# File contenente le variabili d'ambiente sensibili
EnvironmentFile=/var/www/myapp/.env
Assicurarsi che il file .env non sia leggibile da altri utenti:
sudo chmod 600 /var/www/myapp/.env
sudo chown www-data:www-data /var/www/myapp/.env
Log e debug
Per monitorare lo stato del sistema in produzione, i comandi principali sono:
# Stato e log recenti del servizio Gunicorn
sudo systemctl status myapp
sudo journalctl -u myapp -f
# Log di accesso e di errore di nginx
sudo tail -f /var/log/nginx/access.log
sudo tail -f /var/log/nginx/error.log
# Log dell'applicazione
sudo tail -f /var/log/myapp/error.log
Ottimizzazioni per la produzione
Alcune impostazioni aggiuntive migliorano le prestazioni e la robustezza in un ambiente di produzione.
Compressione gzip in nginx
# Da aggiungere nel blocco http di /etc/nginx/nginx.conf
# Abilita la compressione gzip per ridurre la dimensione delle risposte
gzip on;
gzip_comp_level 5;
gzip_min_length 256;
# Tipi di contenuto da comprimere
gzip_types
application/json
application/javascript
text/css
text/html
text/plain;
Rate limiting in nginx
# Zona di limitazione: 10 MB di memoria per gli indirizzi IP, 10 richieste/secondo
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
server {
location /api/ {
# Permette un burst di 20 richieste prima di iniziare a rifiutare
limit_req zone=api_limit burst=20 nodelay;
proxy_pass http://gunicorn_server;
}
}
Timeout e keepalive
upstream gunicorn_server {
server unix:/run/myapp.sock fail_timeout=0;
# Mantiene le connessioni aperte verso Gunicorn per riutilizzarle
keepalive 32;
}
server {
location / {
proxy_pass http://gunicorn_server;
# Necessario per il keepalive con l'upstream
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
Conclusione
Con nginx come reverse proxy e Gunicorn come application server, l'applicazione Python è pronta per un carico di produzione reale. nginx si occupa della terminazione TLS, della compressione, del rate limiting e dei file statici, scaricando il processo Python da tutto ciò che non richiede logica applicativa. Gunicorn, controllato da systemd, garantisce la disponibilità continua del servizio con riavvii automatici in caso di errore. Questa architettura a tre livelli — client, proxy, app — è semplice da manutenere, scalabile orizzontalmente e sufficiente per la grande maggioranza dei casi d'uso in produzione.