Деплой: uvicorn, gunicorn, воркеры

Урок о том, как вывести FastAPI из режима разработки в боевую эксплуатацию: ASGI-сервер, воркеры, обратный прокси и контейнер.

ASGI-сервер — это программа (например, uvicorn), которая принимает HTTP-соединения и запускает ваше асинхронное приложение по стандарту ASGI; сам FastAPI сервером не является.

FastAPI — это фреймворк, а не сервер. Чтобы приложение отвечало на запросы, его запускает ASGI-сервер. Локально вы пишете uvicorn main:app --reload и забываете об этом. Но в проде так делать нельзя: --reload следит за файлами и ест ресурсы, один процесс не использует все ядра, а падение сервера оставляет вас без сервиса. Этот урок — про правильный боевой запуск: сколько процессов поднимать, чем ими управлять, зачем перед ними ставить nginx и как упаковать всё в контейнер.

Зачем это нужно на практике

Разница между dev- и prod-запуском — это разница между «работает у меня» и «держит нагрузку и не падает ночью». В проде вам нужно: задействовать все ядра CPU, переживать падение отдельного воркера, корректно перезапускаться при деплое, отдавать статику и TLS не самим приложением, а тем, кто это делает лучше. Правильная связка серверов решает всё это и стоит недорого в настройке.

uvicorn: ASGI-сервер

uvicorn — основной ASGI-сервер для FastAPI. Он быстрый, потому что построен на uvloop и httptools. Для разработки удобен флаг --reload, для прода — нет.

# разработка: автоперезагрузка при изменении кода
uvicorn main:app --reload

# прод: без reload, слушаем все интерфейсы на нужном порту
uvicorn main:app --host 0.0.0.0 --port 8000

Запомните: --reload в проде — частая и дорогая ошибка. Он держит наблюдатель за файловой системой и не рассчитан на нагрузку. В боевом окружении сервер запускают без него, а число процессов масштабируют отдельно.

gunicorn с uvicorn-воркерами

Сам uvicorn — это один процесс. Чтобы держать несколько процессов и управлять их жизненным циклом (рестарт упавших, плавный перезапуск), классически берут gunicorn как менеджер процессов, а внутри него — uvicorn-воркеры. Gunicorn следит за воркерами, uvicorn внутри каждого исполняет ваше ASGI-приложение.

# gunicorn управляет процессами, класс воркера — uvicorn
gunicorn main:app \
  --workers 4 \
  --worker-class uvicorn.workers.UvicornWorker \
  --bind 0.0.0.0:8000

Здесь --worker-class uvicorn.workers.UvicornWorker — ключевая деталь: без неё gunicorn попытается запустить приложение как синхронное WSGI и упадёт, ведь FastAPI — ASGI. Каждый воркер — отдельный процесс со своим интерпретатором Python; gunicorn перезапускает воркер, если тот умер, и умеет плавный перезапуск по сигналу. Альтернатива — запускать несколько процессов самим uvicorn (--workers N), но gunicorn даёт более зрелое управление.

Сколько воркеров

Главный вопрос: сколько процессов поднимать? Распространённая отправная формула — 2 × количество_ядер + 1. Но для FastAPI важна оговорка: приложение асинхронное, поэтому много операций ввода-вывода один воркер обслуживает конкурентно своим event loop, не требуя процесса на каждый запрос.

Профиль нагрузкиСколько воркеров
В основном ожидание I/O (БД, внешние API), код честно async≈ число ядер; event loop сам разруливает конкурентность
Заметная CPU-работа в обработчикахближе к 2 × ядра + 1, чтобы занять все ядра
Контейнер с лимитом CPU (Kubernetes)считайте по выделенным ядрам, а не по ядрам хоста

Не задирайте число воркеров «на всякий случай»: каждый процесс держит свою копию приложения и потребляет память, а лишние процессы лишь усиливают конкуренцию за CPU. Подбирайте число под реальную нагрузку и измеряйте, а не угадывайте.

За обратным прокси

В проде перед uvicorn почти всегда ставят обратный прокси — обычно nginx. Прокси берёт на себя то, в чём приложение слабо: TLS (HTTPS), отдачу статики, буферизацию медленных клиентов, базовую защиту и балансировку.

server {
    listen 443 ssl;
    server_name api.example.com;

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Заголовки X-Forwarded-For и X-Forwarded-Proto сообщают приложению реальный IP клиента и оригинальную схему (http/https), которые иначе теряются за прокси. Чтобы FastAPI/uvicorn доверял этим заголовкам, сервер запускают с --proxy-headers (и при необходимости --forwarded-allow-ips). Без этого вы будете видеть IP прокси вместо клиента и можете неверно строить ссылки по схеме.

Контейнеризация

Стандарт упаковки сервиса сегодня — Docker-образ: один артефакт, который одинаково запускается и локально, и в проде. Базовый Dockerfile для FastAPI выглядит так:

FROM python:3.12-slim
WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

# не root: меньше прав — меньше ущерба при взломе
RUN useradd --create-home appuser
USER appuser

EXPOSE 8000
CMD ["gunicorn", "main:app", \
     "--workers", "4", \
     "--worker-class", "uvicorn.workers.UvicornWorker", \
     "--bind", "0.0.0.0:8000"]

Несколько практик из урока о безопасности применимы и здесь: ставьте зависимости с --no-cache-dir, копируйте requirements.txt отдельным слоем (тогда кэш сборки переиспользуется, пока зависимости не менялись), запускайте процесс под непривилегированным пользователем через USER. В Kubernetes число воркеров на контейнер часто делают небольшим (1–2), а масштабируют горизонтально, добавляя поды.

Как это работает под капотом

ASGI — это стандартный интерфейс между сервером и асинхронным приложением: сервер для каждого запроса формирует scope (метод, путь, заголовки) и вызывает приложение, передавая ему функции receive и send для чтения тела и отправки ответа. uvicorn реализует HTTP-разбор и event loop, а FastAPI — это и есть ASGI-приложение, которое сервер вызывает. Когда gunicorn запускает uvicorn-воркеры, он работает по модели pre-fork: главный процесс fork-ает дочерние, и они наследуют слушающий сокет, поэтому несколько процессов делят один порт, а ОС раздаёт соединения между ними. Каждый воркер крутит собственный event loop. Плавный перезапуск использует сигналы: gunicorn по HUP поднимает новых воркеров и аккуратно гасит старых, поэтому деплой проходит без обрыва соединений — при условии, что воркеры корректно реагируют на сигнал завершения.

Частые ошибки

  • --reload в проде. Наблюдатель за файлами ест ресурсы и не рассчитан на нагрузку.
  • Забыли UvicornWorker. gunicorn пытается запустить ASGI-приложение как WSGI и падает.
  • Слишком много воркеров. Лишние процессы съедают память и усиливают борьбу за CPU, не давая выигрыша.
  • Нет обратного прокси. Приложение само занимается TLS, статикой и медленными клиентами — не его работа.
  • Не настроен --proxy-headers. За nginx вы видите IP прокси вместо клиента, ссылки строятся по неверной схеме.
  • Контейнер от root. Нарушает least privilege; добавьте USER.

Итоги

  • FastAPI запускает ASGI-сервер (uvicorn); --reload — только для разработки, не для прода.
  • В проде берут gunicorn как менеджер процессов с UvicornWorker — рестарт воркеров и плавный перезапуск.
  • Число воркеров подбирайте под нагрузку: для I/O-bound async-кода ≈ число ядер, для CPU-работы ближе к 2 × ядра + 1.
  • Перед приложением ставьте обратный прокси (nginx) для TLS, статики и балансировки; включайте --proxy-headers.
  • Упаковывайте в Docker: отдельный слой зависимостей, запуск под непривилегированным пользователем; в Kubernetes масштабируйте подами.
Проверьте себя
1. Почему флаг --reload у uvicorn нельзя использовать в продакшене?
AОн отключает HTTPS
BОн держит наблюдатель за файловой системой, ест ресурсы и не рассчитан на нагрузку
CОн работает только с одним воркером
DОн несовместим с Pydantic
2. Что обязательно указать gunicorn, чтобы он смог запустить FastAPI-приложение?
A--worker-class uvicorn.workers.UvicornWorker, иначе gunicorn попытается запустить его как синхронное WSGI и упадёт
B--reload для автоматической перезагрузки
CТолько число воркеров через --workers
DНичего особенного, gunicorn запускает любое приложение
3. Зачем перед uvicorn в продакшене обычно ставят обратный прокси (nginx)?
AЧтобы ускорить выполнение Python-кода
BЧтобы прокси взял на себя TLS, отдачу статики, буферизацию медленных клиентов и балансировку
CПотому что uvicorn не умеет отвечать на HTTP
DЧтобы автоматически создавать воркеры