Как запускать 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
# Копирование кода (пример):
# 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:
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 мог писать.
# Пример 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 понятнее, когда сменились зависимости.
# После 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.