Jak uruchamiać aplikacje Python z Gunicorn i Nginx na VPS
Wdróż aplikację Flask albo FastAPI na Ubuntu z virtualenv, usługą systemd Gunicorn, reverse proxy Nginx, TLS, plikami środowiskowymi, logowaniem i listą kontrolną błędów 502 oraz doboru workerów.

Wbudowany serwer Flask i uvicorn --reload są do developmentu. Na publicznym VPS chcesz menedżer procesów, który restartuje workery, wiąże się z localhost i stoi za reverse proxy obsługującym TLS i wolnych klientów. Gunicorn to zwykły wybór WSGI dla Flask i Django. FastAPI może iść pod Gunicorn z klasą workera Uvicorn. Nginx (albo Caddy) zdejmuje HTTPS i przekazuje na 127.0.0.1:8000.
Ten poradnik prowadzi przez układ, który przeżywa reboot: projekt w /srv/app, virtualenv, .env z sekretami, unit gunicorn.service, blok serwera Nginx, Let's Encrypt i pliki logów, które naprawdę da się grepować. Porozmawiamy też o liczbie workerów, timeoutach i dlaczego 502 Bad Gateway prawie nigdy nie znaczy „Nginx zepsuty” — prawie zawsze Gunicorn nie działa, jest zbindowany do złego gniazda albo sypie się na imporcie. Przykład używa Flask, z uwagami dla FastAPI tam, gdzie komenda się różni.
Dlaczego ten stos
Gunicorn preforkuje procesy workerów. Każdy worker obsługuje jedno żądanie naraz, chyba że użyjesz innej klasy workera. Nginx buforuje wolnych klientów, żeby workery nie tkwiły na wysyłce bajtów do sieci mobilnej. systemd restartuje aplikację, jeśli umrze. Razem to nudne — a właśnie tego chcesz o trzeciej w nocy.
- Gunicorn: stabilny model procesów WSGI/ASGI
- systemd: start przy boot, restart po crashu, logi journald
- Nginx: TLS, pliki statyczne, limity rozmiaru żądania, gzip
- venv: systemowy Python zostaje czysty
- bind na localhost: do aplikacji nie dojdziesz inaczej niż przez Nginx
Wymagania
Python 3.10+ na Ubuntu 22.04/24.04 wystarczy. Nie odpalaj pip jako root do systemowych site-packages. Do TLS potrzebujesz domeny. Jeśli wolisz Caddy jako proxy, unit Gunicorn z tego artykułu zostaje ten sam — zmienia się tylko konfig frontu (zobacz artykuł o Caddy).
- VPS Ubuntu 22.04 lub 24.04
- Twoja aplikacja z requirements.txt albo odpowiednikiem
- entrypoint WSGI (dla Flask: app:app) albo ASGI (dla FastAPI: app:app z workerami uvicorn)
- Rekord A domeny dla HTTPS
Krok 1. Pakiety systemowe, użytkownik i katalog projektu
Utwórz użytkownika systemowego bez interaktywnego logowania, niech posiada kod i uruchamia Gunicorn. Instalacja python3-venv i narzędzi budowania unika błędów pip na pakietach, które nadal kompilują rozszerzenia C.
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/Krok 2. Virtualenv i zależności
Twórz venv jako appuser, żeby właściciele plików się zgadzali. W produkcji przypinaj wersje. Po instalacji potwierdź, że importujesz aplikację w jednorazowym gunicorn --check-config albo imporcie Pythona. Błędy importu tutaj to te same błędy, które później stają się 502.
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')"Krok 3. Plik środowiskowy
Nie hardcoduj SECRET_KEY ani URL bazy w unicie systemd tak, by wylądowały w czytelnym dla świata git. Użyj EnvironmentFile. chmod 640, właściciel root, grupa appuser (albo właściciel appuser, jeśli wolisz).
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/.envKrok 4. Usługa systemd Gunicorn
Binduj na 127.0.0.1:8000, nie 0.0.0.0, chyba że masz powód, by ominąć Nginx. Liczba workerów to często (2 × CPU) + 1 dla sync workerów; na VPS 2 vCPU to 5, co może być za dużo, jeśli każdy worker ładuje ciężki model ML — wtedy 2–3. Dla FastAPI ustaw --worker-class uvicorn.workers.UvicornWorker i zainstaluj uvicorn. Timeouty powinny przekraczać najwolniejsze uczciwe żądanie, nie 30 sekund, jeśli masz eksporty po 2 minuty.
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 8000Krok 5. Reverse proxy Nginx i TLS
Nginx słucha na 80/443 i proxy do Gunicorn. client_max_body_size ma znaczenie przy uploadach. proxy_read_timeout powinien zgadzać się z timeoutem Gunicorn albo go przewyższać. Gdy blok serwera działa na HTTP, wydaj certyfikat Certbotem (albo zmień front na Caddy). Snippet poniżej jest tylko HTTP, żebyś mógł testować; potem odpal Certbot, który potrafi edytować plik.
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.comKrok 6. Pliki statyczne, uprawnienia i specyfika Flask/FastAPI
Nginx powinien serwować statykę, jeśli możesz; jest szybszy niż Gunicorn. Django collectstatic, Flask send_from_directory dla malutkich aplikacji albo później CDN. appuser musi móc czytać drzewo. Jeśli używasz FastAPI, zainstaluj uvicorn[standard] w venv i zmień ExecStart na UvicornWorker. Jeśli używasz gniazd UNIX zamiast TCP, skieruj proxy_pass na gniazdo i dopasuj uprawnienia, żeby www-data mogło do niego pisać.
# 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/appWorkery, pamięć i reload bez przestoju
Sync workery Gunicorn są proste i wystarczą lekkim API żądanie/odpowiedź. Jeśli potrzebujesz wielu równoległych wolnych oczekiwań I/O, rozważ gevent albo workera ASGI — mierz, nie zgaduj. Każdy worker ładuje aplikację; RAM ≈ liczba workerów razy RSS aplikacji. VPS 2 GB z 8 workerami aplikacji 300 MB pójdzie w swap i będzie „losowo wolny”. systemctl reload gunicorn (HUP) potrafi zrestartować workery z nowym kodem, jeśli wdrożyłeś pliki w miejscu; pełny restart jest jaśniejszy, gdy zmieniają się zależności.
# 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/healthRozwiązywanie 502 i cichych crashy
502 znaczy, że Nginx nie dostał poprawnej odpowiedzi od upstream. Pierwsza komenda to journalctl -u gunicorn -e. Częste przyczyny: zła nazwa module:app, brakujący klucz w .env, Postgres nie działa, bind na 127.0.0.1 a Nginx na innym hoście, SELinux (rzadko na Ubuntu) albo aplikacja słucha tylko na IPv6. 504 to timeout. Pętle 301 dzieją się, gdy aplikacja przekierowuje na HTTP, a X-Forwarded-Proto jest ignorowane.
- systemctl status gunicorn — czy jest active?
- journalctl -u gunicorn -n 100 — ImportError, brak env, baza
- curl -v http://127.0.0.1:8000/ z VPS — jeśli to pada, Nginx jest niewinny
- nginx -t i error.log — upstream prematurely closed connection
- ss -tulpn | grep 8000 — nic nie słucha
- Dysk pełny — workery padają w tajemniczy sposób
Bezpieczeństwo
Aplikacja nigdy nie binduje publicznie. Sekrety zostają w .env. Łataj venv i OS. Nie uruchamiaj Gunicorn jako root. Jeśli obsługujesz logowania, ustaw ciasteczka sesji Secure i SameSite i skonfiguruj framework tak, by ufał X-Forwarded-Proto tylko od Nginx. Limitowanie częstotliwości tras logowania w Nginx albo w aplikacji.
- bind 127.0.0.1 albo gniazdo unix
- chmod 600 .env
- User= nie-root w systemd
- TLS przez Certbot albo Caddy
- Wyłącz tryb debug i auto-reload w produkcji
Wskazówki
- Dodaj trasę /health, która sprawdza łączność z DB dla Compose i load balancerów
- Wysyłaj assety statyczne z nagłówkiem cache-control w Nginx
- Użyj osobnego workera albo kolejki (Redis + systemd) na maile i ciężkie zadania
- Przypinaj wersje gunicorn i uvicorn w requirements.txt
- Zrób migawkę przed pierwszym produkcyjnym przełączeniem
Aplikacja Python na VPS jest gotowa do produkcji, gdy działa pod Gunicorn jako usługa systemd, binduje tylko do localhost i jest osiągalna przez Nginx albo Caddy z TLS. Sekrety w EnvironmentFile, workery dobieraj do RAM, a nie do wzoru z bloga, a 502 debuguj najpierw z journala Gunicorn. Gdy ta ścieżka jest udokumentowana dla Twojego repo, każde kolejne wdrożenie to rsync albo git pull, pip install i systemctl restart gunicorn.