Як запускати Python-застосунки через Gunicorn і Nginx на VPS
Викладка Flask або FastAPI на Ubuntu: віртуальне оточення, служба systemd для Gunicorn, зворотний проксі Nginx, TLS, файл змінних, журнали та розбір 502 і числа воркерів.

Вбудований сервер Flask і uvicorn --reload — для розробки. На публічному VPS потрібен менеджер процесів, який перезапускає воркери, слухає localhost і стоїть за зворотним проксі з TLS і повільними клієнтами. Gunicorn — звичайний WSGI-вибір для Flask і Django. FastAPI можна ганяти під Gunicorn із класом воркера Uvicorn. Nginx (або Caddy) завершує HTTPS і пересилає на 127.0.0.1:8000.
Нижче схема, яка переживає перезавантаження: проєкт у /srv/app, віртуальне оточення, .env із секретами, unit gunicorn.service, віртуальний хост Nginx, Let's Encrypt і журнали, які можна grep’ати. Розберемо число воркерів, таймаути й чому 502 Bad Gateway майже ніколи не означає «зламався Nginx»: зазвичай Gunicorn не запущений, слухає не той сокет або падає на імпорті. Приклад на Flask, для FastAPI — окремі зауваження щодо команди.
Навіщо цей стек
Gunicorn заздалегідь піднімає процеси-воркери. Кожен sync-воркер в один момент обробляє один запит, якщо не змінити клас воркера. Nginx буферизує повільних клієнтів, щоб воркери не застрягали на відправці в мобільну мережу. systemd піднімає застосунок після падіння. Разом це нудно — а о третій ночі саме це потрібно.
- Gunicorn: передбачувана модель WSGI/ASGI
- systemd: старт під час завантаження, перезапуск при падінні, журнали journald
- Nginx: TLS, статика, ліміт розміру запиту, gzip
- venv: системний Python не засмічується
- Прив’язка до localhost: до застосунку не достукатися в обхід Nginx
Що потрібно
Python 3.10+ на Ubuntu 22.04/24.04 достатньо. Не ставте pip від root у системні site-packages. Для TLS потрібен домен. Якщо спереду хочете Caddy, unit Gunicorn із цієї статті не змінюється — змінюється лише фронт (див. статтю про Caddy).
- VPS Ubuntu 22.04 або 24.04
- Застосунок із requirements.txt або аналогом
- Точка входу WSGI (для Flask: app:app) або ASGI (FastAPI: app:app і воркери uvicorn)
- A-запис домену для HTTPS
Крок 1. Пакети, користувач і каталог проєкту
Системний користувач без інтерактивного входу володіє кодом і запускає Gunicorn. python3-venv і пакети збірки рятують pip, коли ще трапляються розширення на 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/Крок 2. Віртуальне оточення й залежності
Створюйте venv від appuser, щоб власники файлів збіглися. У бою версії пакетів фіксуйте. Після встановлення переконайтеся, що застосунок імпортується — ті самі помилки потім перетворяться на 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')"Крок 3. Файл змінних оточення
Не зашивайте SECRET_KEY і URL бази в unit так, щоб це витекло в git із правами на читання всім. Використовуйте EnvironmentFile. chmod 640, власник root, група appuser (або власник appuser, якщо так зручніше).
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Крок 4. Служба systemd для Gunicorn
Слухайте 127.0.0.1:8000, не 0.0.0.0, якщо немає причини обходити Nginx. Число sync-воркерів часто (2 × CPU) + 1; на 2 vCPU це 5, і цього може бути багато, якщо кожен воркер вантажить важку модель — тоді 2–3. Для FastAPI вкажіть --worker-class uvicorn.workers.UvicornWorker і поставте uvicorn. Таймаут має бути більшим за найдовший чесний запит, а не 30 секунд, якщо у вас вивантаження по дві хвилини.
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Крок 5. Зворотний проксі Nginx і TLS
Nginx слухає 80/443 і проксує на Gunicorn. client_max_body_size важливий для завантажень. proxy_read_timeout не менший за таймаут Gunicorn. Коли HTTP запрацює, випустіть сертифікат Certbot (або поставте спереду Caddy). Нижче блок лише HTTP для перевірки; Certbot потім може його правити.
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Крок 6. Статика, права й відмінності Flask/FastAPI
Статику за можливості віддає Nginx — це швидше за Gunicorn. Django collectstatic, для крихітного Flask іноді send_from_directory, пізніше CDN. Дерево має читати appuser. Для FastAPI поставте uvicorn[standard] у venv і змініть ExecStart на UvicornWorker. Якщо берете UNIX-сокет замість TCP, proxy_pass дивиться в сокет, права такі, щоб www-data міг писати.
# 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Воркери, пам’ять і перезавантаження без зайвої драми
Синхронні воркери Gunicorn прості й підходять легким API запит-відповідь. Багато одночасних повільних I/O — дивіться gevent або ASGI, але вимірюйте, не гадайте. Кожен воркер завантажує застосунок; RAM ≈ число воркерів × RSS застосунку. VPS на 2 ГБ і 8 воркерів по 300 МБ підуть у підкачку й будуть «іноді гальмувати». systemctl reload gunicorn (HUP) перезапускає воркери з новим кодом, якщо файли вже на місці; повний restart зрозуміліший, коли змінилися залежності.
# 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Розбір 502 і тихих падінь
502 означає, що Nginx не отримав нормальну відповідь від upstream. Перша команда — journalctl -u gunicorn -e. Часті причини: невірне module:app, немає ключа в .env, не запущений Postgres, слухає 127.0.0.1, а Nginx на іншому хості, SELinux (рідко на Ubuntu) або застосунок слухає лише IPv6. 504 — таймаут. Цикл 301 — застосунок редиректить на HTTP і ігнорує X-Forwarded-Proto.
- systemctl status gunicorn — служба active?
- journalctl -u gunicorn -n 100 — ImportError, немає змінної, база
- curl -v http://127.0.0.1:8000/ із самого VPS — якщо тут помилка, Nginx ні до чого
- nginx -t і error.log — upstream prematurely closed connection
- ss -tulpn | grep 8000 — ніхто не слухає
- Диск забитий — воркери падають «незрозуміло чому»
Захист
Застосунок зовні не слухає. Секрети в .env. venv і ОС оновлюйте. Gunicorn не від root. Якщо є вхід користувачів, cookie сесії Secure і SameSite, фреймворк довіряє X-Forwarded-Proto лише від Nginx. Ліміт частоти на логін — у Nginx або в застосунку.
- bind 127.0.0.1 або UNIX-сокет
- chmod 600 у .env
- User= не root у systemd
- TLS через Certbot або Caddy
- debug і автоперезавантаження в бою вимкнені
Поради
- Маршрут /health із перевіркою бази для Compose і балансувальників
- Для статики в Nginx задайте Cache-Control
- Листи й важкі задачі — окремий воркер або черга (Redis + systemd)
- Версії gunicorn і uvicorn фіксуйте в requirements.txt
- Знімок диска перед першим бойовим перемиканням DNS
Python-застосунок на VPS готовий до бою, коли він крутиться під Gunicorn як служба systemd, слухає лише localhost і доступний через Nginx або Caddy з TLS. Секрети — в EnvironmentFile, число воркерів — за RAM, а не за чужою формулою з блогу, 502 спочатку дивіться в журналі Gunicorn. Коли цей шлях записаний для репозиторію, кожна наступна викладка — rsync або git pull, pip install і systemctl restart gunicorn.