Python-Apps mit Gunicorn und Nginx auf einem VPS betreiben
Eine Flask- oder FastAPI-Anwendung auf Ubuntu mit virtualenv, Gunicorn-systemd-Dienst, Nginx-Reverse-Proxy, TLS, Umgebungsdateien, Logging und einer Checkliste für 502-Fehler und Worker-Dimensionierung bereitstellen.

Der eingebaute Flask-Server und uvicorn --reload sind für die Entwicklung. Auf einem öffentlichen VPS wollen Sie einen Prozessmanager, der Worker neu startet, an localhost bindet und hinter einem Reverse-Proxy sitzt, der TLS und langsame Clients übernimmt. Gunicorn ist die übliche WSGI-Wahl für Flask und Django. FastAPI kann unter Gunicorn mit einer Uvicorn-Worker-Klasse laufen. Nginx (oder Caddy) terminiert HTTPS und leitet nach 127.0.0.1:8000 weiter.
Diese Anleitung führt durch ein Layout, das Reboots überlebt: Projekt in /srv/app, virtualenv, .env mit Secrets, eine gunicorn.service-Unit, Nginx-Server-Block, Let's Encrypt und Logdateien, die Sie wirklich greppen können. Wir sprechen auch über Worker-Anzahl, Timeouts und warum 502 Bad Gateway fast nie „Nginx ist kaputt“ bedeutet — fast immer läuft Gunicorn nicht, ist an den falschen Socket gebunden oder stürzt beim Import ab. Das Beispiel nutzt Flask, mit Hinweisen zu FastAPI, wo der Befehl abweicht.
Warum dieser Stack
Gunicorn forkt Worker-Prozesse vor. Jeder Worker bearbeitet eine Anfrage zur Zeit, sofern Sie keine andere Worker-Klasse nutzen. Nginx puffert langsame Clients, damit Worker nicht Bytes in ein Mobilnetz schicken. systemd startet die App neu, wenn sie stirbt. Zusammen ist das langweilig — genau das, was Sie um 3 Uhr nachts wollen.
- Gunicorn: stabiles WSGI/ASGI-Prozessmodell
- systemd: Start beim Boot, Neustart nach Absturz, journald-Logs
- Nginx: TLS, statische Dateien, Request-Größenlimits, gzip
- venv: System-Python bleibt sauber
- localhost-Bind: die App ist nur über Nginx erreichbar
Voraussetzungen
Python 3.10+ auf Ubuntu 22.04/24.04 ist in Ordnung. pip nicht als root in systemweite site-packages ausführen. Für TLS brauchen Sie eine Domain. Bevorzugen Sie Caddy als Proxy, bleibt die Gunicorn-Unit in diesem Artikel gleich — nur die Frontend-Config ändert sich (siehe Caddy-Artikel).
- Ubuntu 22.04 oder 24.04 VPS
- Ihre Anwendung mit requirements.txt oder Entsprechung
- Ein WSGI-Entrypoint (Flask: app:app) oder ASGI (FastAPI: app:app mit uvicorn-Workern)
- Domain-A-Record für HTTPS
Schritt 1: Systempakete, Benutzer und Projektverzeichnis
Legen Sie einen Systembenutzer ohne interaktives Login an, dem der Code gehört und der Gunicorn ausführt. python3-venv und Build-Tools vermeiden pip-Fehler bei Paketen, die noch C-Erweiterungen kompilieren.
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/Schritt 2: Virtualenv und Abhängigkeiten
Das venv als appuser anlegen, damit die Dateibesitzer stimmen. In Produktion Versionen pinnen. Nach der Installation prüfen, ob Sie die App in einem einmaligen gunicorn --check-config oder einem Python-Import laden können. Importfehler hier sind dieselben, die später zu 502 werden.
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')"Schritt 3: Umgebungsdatei
SECRET_KEY oder Datenbank-URLs nicht so in der systemd-Unit hartcodieren, dass sie in weltlesbarem Git landen. EnvironmentFile nutzen. chmod 640, Owner root, Gruppe appuser (oder Owner appuser, wenn Sie das bevorzugen).
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/.envSchritt 4: Gunicorn-systemd-Dienst
An 127.0.0.1:8000 binden, nicht 0.0.0.0, außer Sie wollen Nginx überspringen. Die Worker-Zahl ist oft (2 × CPU) + 1 für Sync-Worker; auf einem 2-vCPU-VPS sind das 5, was zu viel sein kann, wenn jeder Worker ein schweres ML-Modell lädt — dann 2–3. Für FastAPI --worker-class uvicorn.workers.UvicornWorker setzen und uvicorn installieren. Timeouts sollten die langsamste ehrliche Anfrage übersteigen, nicht 30 Sekunden, wenn Sie 2-Minuten-Exports haben.
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 8000Schritt 5: Nginx-Reverse-Proxy und TLS
Nginx lauscht auf 80/443 und proxyt zu Gunicorn. client_max_body_size zählt bei Uploads. proxy_read_timeout sollte Gunicorns Timeout erreichen oder überschreiten. Funktioniert der Server-Block per HTTP, ein Zertifikat mit Certbot ausstellen (oder das Frontend auf Caddy umstellen). Das Snippet unten ist nur HTTP zum Testen; danach Certbot, das die Datei anpassen kann.
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.comSchritt 6: Statische Dateien, Rechte und Flask/FastAPI-Besonderheiten
Nginx sollte statische Dateien ausliefern, wenn möglich; das ist schneller als Gunicorn. Django collectstatic, Flask send_from_directory für Mini-Apps oder später ein CDN. appuser muss den Baum lesen können. Bei FastAPI uvicorn[standard] im venv installieren und ExecStart auf UvicornWorker umstellen. Bei Unix-Sockets statt TCP proxy_pass auf den Socket zeigen und Rechte so setzen, dass www-data schreiben kann.
# 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/appWorker, Speicher und Reloads ohne Downtime
Sync-Gunicorn-Worker sind einfach und reichen für CPU-leichte Request/Response-APIs. Brauchen Sie viele gleichzeitige langsame I/O-Wartezeiten, gevent oder einen ASGI-Worker in Betracht ziehen — messen, nicht raten. Jeder Worker lädt Ihre App; RAM ≈ Worker mal App-RSS. Ein 2-GB-VPS mit 8 Workern einer 300-MB-App swapped und fühlt sich „zufällig langsam“ an. systemctl reload gunicorn (HUP) kann Worker mit neuem Code neu starten, wenn Dateien an Ort und Stelle deployed wurden; ein voller Restart ist klarer, wenn sich Abhängigkeiten ändern.
# 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/health502 und stille Abstürze beheben
502 heißt, Nginx bekam keine gültige Antwort vom Upstream. journalctl -u gunicorn -e ist der erste Befehl. Häufige Ursachen: falscher module:app-Name, fehlender .env-Schlüssel, Postgres läuft nicht, an 127.0.0.1 gebunden aber Nginx auf einem anderen Host, SELinux (selten auf Ubuntu) oder die App lauscht nur auf IPv6. 504 ist ein Timeout. 301-Schleifen entstehen, wenn die App nach HTTP umleitet, während X-Forwarded-Proto ignoriert wird.
- systemctl status gunicorn — ist der Dienst aktiv?
- journalctl -u gunicorn -n 100 — ImportError, fehlende Env, Datenbank
- curl -v http://127.0.0.1:8000/ vom VPS — schlägt das fehl, ist Nginx unschuldig
- nginx -t und error.log — upstream prematurely closed connection
- ss -tulpn | grep 8000 — nichts lauscht
- Platte voll — Worker stürzen auf rätselhafte Weise ab
Sicherheit
Die App bindet niemals öffentlich. Secrets bleiben in .env. venv und OS gepatcht halten. Gunicorn nicht als root betreiben. Bei Logins Session-Cookies Secure und SameSite setzen und das Framework so konfigurieren, dass es X-Forwarded-Proto nur von Nginx vertraut. Login-Routen in Nginx oder in der App rate-limiten.
- an 127.0.0.1 oder einen Unix-Socket binden
- chmod 600 .env
- Non-root User= in systemd
- TLS über Certbot oder Caddy
- Debug-Modus und Auto-Reload in Produktion deaktivieren
Tipps
- Eine /health-Route, die die DB-Verbindung prüft, für Compose und Load Balancer
- Statische Assets mit Cache-Control-Header in Nginx ausliefern
- Separaten Worker oder Queue (Redis + systemd) für E-Mails und schwere Jobs
- gunicorn- und uvicorn-Versionen in requirements.txt pinnen
- Vor dem ersten Produktions-Cutover einen Snapshot machen
Eine Python-App auf einem VPS ist produktionsreif, wenn sie unter Gunicorn als systemd-Dienst läuft, nur an localhost bindet und über Nginx oder Caddy mit TLS erreichbar ist. Secrets in ein EnvironmentFile, Worker nach RAM statt nach Blog-Formel dimensionieren und 502 zuerst im Gunicorn-Journal debuggen. Ist dieser Weg für Ihr Repo dokumentiert, ist jedes spätere Deploy rsync oder git pull, pip install und systemctl restart gunicorn.