Torna al blog
Agosto 19, 2026Guide

Come eseguire app Python con Gunicorn e Nginx su un VPS

Distribuire un’applicazione Flask o FastAPI su Ubuntu con un virtualenv, un servizio systemd Gunicorn, un reverse proxy Nginx, TLS, file di ambiente, logging e una lista di controllo per gli errori 502 e il dimensionamento dei worker.

Come eseguire app Python con Gunicorn e Nginx su un VPS

Il server Flask integrato e uvicorn --reload sono per lo sviluppo. Su un VPS pubblico vuoi un gestore di processi che riavvii i worker, si associ a localhost e stia dietro un reverse proxy che gestisce TLS e client lenti. Gunicorn è la scelta WSGI usuale per Flask e Django. FastAPI può girare sotto Gunicorn con una classe worker Uvicorn. Nginx (o Caddy) termina HTTPS e inoltra a 127.0.0.1:8000.

Questa guida percorre un layout che sopravvive ai riavvii: progetto in /srv/app, virtualenv, .env con i segreti, un’unità gunicorn.service, un blocco server Nginx, Let's Encrypt e file di log che puoi davvero greppare. Parleremo anche del numero di worker, dei timeout e del perché 502 Bad Gateway quasi mai è «Nginx è rotto»: quasi sempre Gunicorn non gira, è associato al socket sbagliato o crasha all’import. L’esempio usa Flask, con note per FastAPI dove il comando differisce.

Perché questo stack

Gunicorn pre-esegue fork dei processi worker. Ogni worker gestisce una richiesta alla volta a meno che tu non usi una classe worker diversa. Nginx bufferizza i client lenti così i worker non restano bloccati a inviare byte verso una rete mobile. systemd riavvia l’app se muore. Insieme è noioso, che è ciò che vuoi alle tre di notte.

  • Gunicorn: modello di processo WSGI/ASGI stabile
  • systemd: avvio al boot, riavvio in caso di crash, log journald
  • Nginx: TLS, file statici, limiti di dimensione delle richieste, gzip
  • venv: il Python di sistema resta pulito
  • bind su localhost: l’app non è raggiungibile se non tramite Nginx

Requisiti

Python 3.10+ su Ubuntu 22.04/24.04 va bene. Non eseguire pip come root nei site-packages di sistema. Ti serve un dominio per TLS. Se preferisci Caddy come proxy, l’unità Gunicorn di questo articolo resta uguale: cambia solo la config del front-end (vedi l’articolo su Caddy).

  • VPS Ubuntu 22.04 o 24.04
  • La tua applicazione con un requirements.txt o equivalente
  • Un entrypoint WSGI (per Flask: app:app) o ASGI (per FastAPI: app:app con worker uvicorn)
  • Record A del dominio per HTTPS

Passo 1: Pacchetti di sistema, utente e directory del progetto

Crea un utente di sistema che non può accedere in modo interattivo, proprietario del codice, e che esegue Gunicorn. Installare python3-venv e gli strumenti di build evita fallimenti pip su pacchetti che compilano ancora estensioni C.

bash
ssh root@YOUR_VPS_IP
apt update && apt -y upgrade
apt -y install python3 python3-venv python3-pip python3-dev build-essential nginx curl

adduser --system --group --home /srv/app appuser
mkdir -p /srv/app
chown appuser:appuser /srv/app

# Copy your code (example):
# rsync -a --delete ./myproject/ appuser@YOUR_VPS_IP:/srv/app/

Passo 2: Virtualenv e dipendenze

Crea il venv come appuser così la proprietà dei file è corretta. Fissa le versioni in produzione. Dopo l’installazione, conferma di poter importare l’app con un gunicorn --check-config occasionale o un import Python. Gli errori di import qui sono gli stessi che più tardi diventano 502.

bash
sudo -u appuser -H bash -lc '
cd /srv/app
python3 -m venv /srv/app/venv
/srv/app/venv/bin/pip install --upgrade pip
/srv/app/venv/bin/pip install -r /srv/app/requirements.txt gunicorn
'

# Flask example check:
sudo -u appuser -H /srv/app/venv/bin/python -c "from app import app; print('import ok')"

Passo 3: File di ambiente

Non inserire SECRET_KEY o URL di database nell’unità systemd in un modo che finisca in un git leggibile da tutti. Usa EnvironmentFile. chmod 640, proprietario root, gruppo appuser (o proprietario appuser se preferisci).

bash
cat >/srv/app/.env <<'EOF'
FLASK_ENV=production
SECRET_KEY=replace-with-openssl-rand-hex-32
DATABASE_URL=postgresql://app:password@127.0.0.1:5432/app
EOF
chown appuser:appuser /srv/app/.env
chmod 600 /srv/app/.env

Passo 4: Servizio systemd Gunicorn

Associa a 127.0.0.1:8000, non a 0.0.0.0, a meno che tu abbia un motivo per saltare Nginx. Il numero di worker è spesso (2 × CPU) + 1 per i worker sync; su un VPS 2 vCPU sono 5, che può essere troppo se ogni worker carica un modello ML pesante: allora usa 2–3. Per FastAPI imposta --worker-class uvicorn.workers.UvicornWorker e installa uvicorn. I timeout devono superare la richiesta onesta più lenta, non 30 secondi se hai export da 2 minuti.

bash
cat >/etc/systemd/system/gunicorn.service <<'EOF'
[Unit]
Description=Gunicorn for the web app
After=network.target

[Service]
User=appuser
Group=appuser
WorkingDirectory=/srv/app
EnvironmentFile=/srv/app/.env
ExecStart=/srv/app/venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 --timeout 60 --access-logfile - --error-logfile - app:app
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now gunicorn
systemctl status gunicorn --no-pager
ss -tulpn | grep 8000

Passo 5: Reverse proxy Nginx e TLS

Nginx ascolta su 80/443 e fa proxy verso Gunicorn. client_max_body_size conta per gli upload. proxy_read_timeout deve eguagliare o superare il timeout di Gunicorn. Quando il blocco server funziona in HTTP, emetti un certificato con Certbot (o passa il front a Caddy). Lo snippet sotto è solo HTTP per testare; poi esegui Certbot, che può modificare il file.

bash
cat >/etc/nginx/sites-available/app <<'EOF'
server {
    listen 80;
    server_name app.example.com;
    client_max_body_size 20m;

    location /static/ {
        alias /srv/app/static/;
    }

    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;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 90s;
    }
}
EOF

ln -sf /etc/nginx/sites-available/app /etc/nginx/sites-enabled/app
nginx -t && systemctl reload nginx

ufw allow OpenSSH
ufw allow 'Nginx Full'
ufw enable

apt -y install certbot python3-certbot-nginx
certbot --nginx -d app.example.com

Passo 6: File statici, permessi e specificità Flask/FastAPI

Nginx dovrebbe servire i file statici se puoi; è più veloce di Gunicorn. Django collectstatic, Flask send_from_directory per app minuscole, o un CDN più tardi. appuser deve poter leggere l’albero. Se usi FastAPI, installa uvicorn[standard] nel venv e cambia ExecStart per usare UvicornWorker. Se usi socket Unix invece di TCP, punta proxy_pass al socket e allinea i permessi così www-data può scriverci.

bash
# FastAPI ExecStart example:
# ExecStart=/srv/app/venv/bin/gunicorn -k uvicorn.workers.UvicornWorker --workers 2 --bind 127.0.0.1:8000 app:app

# Unix socket variant:
# --bind unix:/run/gunicorn/app.sock
# Nginx: proxy_pass http://unix:/run/gunicorn/app.sock:

chown -R appuser:appuser /srv/app

Worker, memoria e reload a zero downtime

I worker sync di Gunicorn sono semplici e bastano per API richiesta/risposta leggere in CPU. Se ti servono molte attese I/O lente concorrenti, valuta gevent o un worker ASGI: misura, non indovinare. Ogni worker carica la tua app; RAM ≈ worker per l’RSS dell’app. Un VPS da 2 GB con 8 worker di un’app da 300 MB farà swap e sembrerà «lento a caso». systemctl reload gunicorn (HUP) può riavviare i worker con il codice nuovo se hai distribuito i file sul posto; un riavvio completo è più chiaro quando cambiano le dipendenze.

bash
# After git pull / rsync of new code:
sudo -u appuser -H /srv/app/venv/bin/pip install -r /srv/app/requirements.txt
systemctl restart gunicorn
curl -I https://app.example.com/health

Risolvere 502 e crash silenziosi

502 significa che Nginx non ha ottenuto una risposta valida dall’upstream. journalctl -u gunicorn -e è il primo comando. Cause comuni: nome module:app sbagliato, chiave .env mancante, Postgres spento, associato a 127.0.0.1 ma Nginx su un altro host, SELinux (raro su Ubuntu) o l’app in ascolto solo su IPv6. 504 è un timeout. I loop 301 succedono quando l’app reindirizza a HTTP mentre X-Forwarded-Proto viene ignorato.

  • systemctl status gunicorn — è attivo?
  • journalctl -u gunicorn -n 100 — ImportError, env mancante, database
  • curl -v http://127.0.0.1:8000/ dal VPS — se fallisce, Nginx è innocente
  • nginx -t e error.log — upstream prematurely closed connection
  • ss -tulpn | grep 8000 — niente in ascolto
  • Disco pieno — i worker crashano in modi misteriosi

Sicurezza

L’app non si associa mai in pubblico. I segreti restano in .env. Tieni venv e sistema patchati. Non eseguire Gunicorn come root. Se gestisci accessi, imposta i cookie di sessione Secure e SameSite e configura il framework perché si fidi di X-Forwarded-Proto solo da Nginx. Limita la frequenza delle rotte di login in Nginx o nell’app.

  • associare 127.0.0.1 o un socket Unix
  • chmod 600 .env
  • User= non root in systemd
  • TLS tramite Certbot o Caddy
  • Disattiva la modalità debug e l’auto-reload in produzione

Consigli

  • Aggiungi una rotta /health che verifica la connettività al DB per Compose e i load balancer
  • Consegna gli asset statici con un’intestazione cache-control in Nginx
  • Usa un worker o una coda separati (Redis + systemd) per email e lavori pesanti
  • Fissa le versioni di gunicorn e uvicorn in requirements.txt
  • Fai uno snapshot prima del primo passaggio in produzione

Un’app Python su un VPS è pronta per la produzione quando gira sotto Gunicorn come servizio systemd, si associa solo a localhost ed è raggiunta tramite Nginx o Caddy con TLS. Metti i segreti in un EnvironmentFile, dimensiona i worker in base alla RAM e non a una formula da blog, e fai debug dei 502 prima dal journal di Gunicorn. Una volta documentato questo percorso per il tuo repo, ogni deploy successivo è rsync o git pull, pip install e systemctl restart gunicorn.