Назад до блогу
Серпень 19, 2026Посібники

Як запускати Python-застосунки через Gunicorn і Nginx на VPS

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

Як запускати Python-застосунки через Gunicorn і Nginx на VPS

Вбудований сервер 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.

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/

Крок 2. Віртуальне оточення й залежності

Створюйте venv від appuser, щоб власники файлів збіглися. У бою версії пакетів фіксуйте. Після встановлення переконайтеся, що застосунок імпортується — ті самі помилки потім перетворяться на 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')"

Крок 3. Файл змінних оточення

Не зашивайте SECRET_KEY і URL бази в unit так, щоб це витекло в git із правами на читання всім. Використовуйте EnvironmentFile. chmod 640, власник root, група appuser (або власник appuser, якщо так зручніше).

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

Крок 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 секунд, якщо у вас вивантаження по дві хвилини.

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

Крок 5. Зворотний проксі Nginx і TLS

Nginx слухає 80/443 і проксує на Gunicorn. client_max_body_size важливий для завантажень. proxy_read_timeout не менший за таймаут Gunicorn. Коли HTTP запрацює, випустіть сертифікат Certbot (або поставте спереду Caddy). Нижче блок лише HTTP для перевірки; Certbot потім може його правити.

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

Крок 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 міг писати.

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

Воркери, пам’ять і перезавантаження без зайвої драми

Синхронні воркери Gunicorn прості й підходять легким API запит-відповідь. Багато одночасних повільних I/O — дивіться gevent або ASGI, але вимірюйте, не гадайте. Кожен воркер завантажує застосунок; RAM ≈ число воркерів × RSS застосунку. VPS на 2 ГБ і 8 воркерів по 300 МБ підуть у підкачку й будуть «іноді гальмувати». systemctl reload gunicorn (HUP) перезапускає воркери з новим кодом, якщо файли вже на місці; повний restart зрозуміліший, коли змінилися залежності.

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

Розбір 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.