Назад к блогу
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

# Копирование кода (пример):
# 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:
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
# Пример ExecStart для FastAPI:
# ExecStart=/srv/app/venv/bin/gunicorn -k uvicorn.workers.UvicornWorker --workers 2 --bind 127.0.0.1:8000 app:app

# Вариант с UNIX-сокетом:
# --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, но измеряйте, не гадайте. Каждый воркер загружает приложение; ОЗУ ≈ число воркеров × RSS приложения. VPS на 2 ГБ и 8 воркеров по 300 МБ уйдут в подкачку и будут «иногда тормозить». systemctl reload gunicorn (HUP) перезапускает воркеры с новым кодом, если файлы уже на месте; полный restart понятнее, когда сменились зависимости.

bash
# После git pull / rsync нового кода:
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 на другом хосте, или приложение слушает только 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, число воркеров — по ОЗУ, а не по чужой формуле из блога, 502 сначала смотрите в журнале Gunicorn. Когда этот путь записан для репозитория, каждая следующая выкладка — rsync или git pull, pip install и systemctl restart gunicorn.