MHOSTMHOST

Python и FastAPI: продакшн-деплой

После этой инструкции ваше FastAPI-приложение будет работать под управлением Gunicorn с воркерами Uvicorn, автоматически подниматься при перезагрузке сервера через systemd и обслуживать внешние запросы через nginx — то есть будет готово к реальному трафику, а не только к uvicorn main:app --reload на ноутбуке.

Что понадобится

  • VPS mHost на одном из шаблонов ОС: Ubuntu 22.04/24.04, Debian 11/12, AlmaLinux/Rocky Linux 8/9.
  • Доступ по SSH под root или пользователем с sudo.
  • Домен, указывающий на IP сервера — для server_name в nginx. Если домена пока нет, подойдёт и IP-адрес.
  • Готовое FastAPI-приложение (или минимальный пример из статьи — просто чтобы проверить всю цепочку от venv до nginx).
Совет: отдельного готового рецепта именно для FastAPI в VMmanager 6 нет. Ближе всего рецепт Django (Django + uWSGI + Nginx, ID 1) — но он рассчитан на Django, а не на связку FastAPI/Gunicorn/Uvicorn. Полный список того, что ставится в один клик — в статье «Готовые рецепты VMmanager 6». Ниже — ручная установка, которая подходит для любого приложения на FastAPI.

Шаг 1. Подготовьте пользователя, venv и зависимости

Приложение будет работать не от root, а от отдельного системного пользователя без права входа — это ограничивает ущерб, если в самом приложении найдётся уязвимость.

Установите Python и модуль для виртуальных окружений:

Ubuntu / Debian

bash
sudo apt update
sudo apt install python3-venv python3-pip

AlmaLinux / Rocky Linux

bash
sudo dnf install python3 python3-pip

Создайте отдельного системного пользователя для приложения. Флаг --create-home заодно создаст ему домашний каталог /opt/myapp, где будут жить и код, и venv:

bash
sudo useradd --system --create-home --home-dir /opt/myapp --shell /usr/sbin/nologin myapp

Переключитесь на этого пользователя (у него нет обычного шелла для входа, поэтому запускаем bash явной командой) и создайте виртуальное окружение:

bash
sudo -u myapp -H bash
cd /opt/myapp
python3 -m venv venv
source venv/bin/activate

Установите FastAPI, Uvicorn и Gunicorn:

bash
pip install --upgrade pip
pip install fastapi "uvicorn[standard]" gunicorn uvicorn-worker
Совет: пакет uvicorn-worker — не лишний. Класс uvicorn.workers.UvicornWorker, который раньше шёл в комплекте с самим uvicorn, в официальной документации Uvicorn помечен как deprecated и вынесен в отдельный пакет uvicorn-worker (класс uvicorn_worker.UvicornWorker). Именно на него и нужно указывать флагом --worker-class — старый путь ещё может работать в вашей версии Uvicorn, но полагаться на него уже не стоит.

Создайте минимальный main.py — если у вас уже есть готовое приложение, пропустите этот шаг и просто скопируйте свой код в /opt/myapp:

bash
cat > /opt/myapp/main.py << 'EOF'
from fastapi import FastAPI

app = FastAPI()


@app.get("/")
def read_root():
    return {"status": "ok"}
EOF

Проверьте, что приложение вообще запускается — напрямую через Uvicorn, без Gunicorn:

bash
uvicorn main:app --host 127.0.0.1 --port 8000 &
sleep 1
curl http://127.0.0.1:8000/
kill %1

Должно вернуться {"status":"ok"}.

Шаг 2. Запустите Gunicorn с воркерами Uvicorn

Gunicorn выступает менеджером процессов: поднимает несколько параллельных воркеров, перезапускает упавшие и умеет обновлять приложение без даунтайма — но сам протокол ASGI, на котором работает FastAPI, Gunicorn понимает только через класс воркера uvicorn_worker.UvicornWorker.

Число воркеров задаётся флагом -w/--workers. Официальная рекомендация Gunicorn — 2–4 воркера на ядро CPU (число ядер можно узнать командой nproc). Для небольшого VPS в 1–2 ядра обычно достаточно 3–4 воркеров.

Проверьте связку прямо сейчас, всё ещё находясь в сессии пользователя myapp с активным venv:

bash
gunicorn main:app \
    --workers 3 \
    --worker-class uvicorn_worker.UvicornWorker \
    --bind 127.0.0.1:8000 &
sleep 1
curl http://127.0.0.1:8000/
kill %1
exit

Команда exit в конце возвращает вас обратно в сессию sudo-пользователя — дальше вся настройка идёт от его имени.

Шаг 3. Настройте systemd-юнит

Создайте юнит /etc/systemd/system/myapp.service:

bash
sudo tee /etc/systemd/system/myapp.service > /dev/null << 'EOF'
[Unit]
Description=Gunicorn (Uvicorn workers) for myapp
After=network.target

[Service]
Type=notify
NotifyAccess=main
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
Environment=PYTHONUNBUFFERED=1
ExecStart=/opt/myapp/venv/bin/gunicorn main:app \
    --workers 3 \
    --worker-class uvicorn_worker.UvicornWorker \
    --bind 127.0.0.1:8000 \
    --access-logfile - \
    --error-logfile - \
    --timeout 60
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=mixed
TimeoutStopSec=5
Restart=on-failure
RestartSec=5
PrivateTmp=true

[Install]
WantedBy=multi-user.target
EOF
Совет: Type=notify — официально рекомендованный Gunicorn способ (поддерживается с версии 20.0: Gunicorn сам уведомляет systemd о готовности через sd_notify). Если после запуска юнит зависает в состоянии activating и падает по таймауту — замените Type=notify на Type=simple, это известный edge-case на некоторых версиях.

Включите и запустите сервис:

bash
sudo systemctl daemon-reload
sudo systemctl enable --now myapp
sudo systemctl status myapp

Логи (в том числе логи запросов, которые пишет Gunicorn) смотрите через journald:

bash
sudo journalctl -u myapp -f

Шаг 4. Настройте nginx как реверс-прокси

Установите nginx:

Ubuntu / Debian

bash
sudo apt install nginx

AlmaLinux / Rocky Linux

bash
sudo dnf install nginx
sudo systemctl enable --now nginx

Создайте конфиг сайта. Путь к файлу отличается в зависимости от дистрибутива — выберите нужную строку CONF=:

bash
# Ubuntu / Debian:
CONF=/etc/nginx/sites-available/myapp
# AlmaLinux / Rocky Linux:
CONF=/etc/nginx/conf.d/myapp.conf

sudo tee "$CONF" > /dev/null << 'EOF'
upstream myapp {
    server 127.0.0.1:8000 fail_timeout=0;
}

server {
    listen 80;
    server_name example.com;

    client_max_body_size 20m;

    location / {
        proxy_pass http://myapp;
        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;
    }
}
EOF

# Ubuntu / Debian only — enable the site with a symlink:
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/myapp

Замените example.com на свой домен (или на IP-адрес сервера, если домена ещё нет). fail_timeout=0 в блоке upstream значит «не считать бэкенд недоступным после одной неудачной попытки, пробовать снова» — это разумно, когда за upstream стоит единственный процесс Gunicorn и переключаться всё равно больше не на что.

Проверьте конфигурацию и перезапустите nginx:

bash
sudo nginx -t
sudo systemctl reload nginx

Проверьте снаружи:

bash
curl http://example.com/

Должно вернуться {"status":"ok"}.

Совет (AlmaLinux/Rocky Linux): по умолчанию SELinux не разрешает nginx открывать исходящие сетевые соединения — реверс-прокси в этом случае будет отвечать 502 Bad Gateway. Проверьте булево значение httpd_can_network_connect и включите его при необходимости:
bash
getsebool httpd_can_network_connect
sudo setsebool -P httpd_can_network_connect on
Важно: чтобы порт 80 был доступен снаружи, его нужно открыть в файрволе — см. шаг 5 ниже или статью «Настройка firewall (ufw)».

Шаг 5. Откройте порт в файрволе

Сам Gunicorn слушает только 127.0.0.1 и снаружи недоступен в принципе — открывать нужно только порт nginx.

Важно: прежде чем включать файрвол, убедитесь, что порт SSH (обычно 22/tcp) точно разрешён — иначе можно потерять доступ к серверу.

Ubuntu / Debian (ufw)

bash
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw enable
sudo ufw status verbose

AlmaLinux / Rocky Linux (firewalld)

bash
sudo firewall-cmd --permanent --add-service=ssh
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --reload
sudo firewall-cmd --list-all
Совет: на свежем образе SSH обычно уже разрешён в firewalld по умолчанию, но повторно добавить правило безопасно — ошибки не будет. Если позже добавите HTTPS (например, Let's Encrypt/certbot), не забудьте так же открыть 443/tcp (ufw allow 443/tcp или --add-service=https). Подробнее о работе с файрволом — в статье «Настройка firewall (ufw)».
Готово: приложение доступно снаружи по адресу http://example.com/ (или по IP), работает под управлением systemd — переживёт перезагрузку сервера и падение отдельных воркеров — и стоит за nginx.

Что дальше

  • Логичный следующий шаг — выпустить TLS-сертификат и включить HTTPS (например, через Let's Encrypt/certbot) поверх уже настроенного nginx; это отдельная тема за рамками этой статьи.
  • Обновлять приложение теперь просто: обновите код в /opt/myapp (например, git pull от имени пользователя myapp) и перезапустите сервис командой sudo systemctl restart myapp.
  • Полный список того, что в VMmanager 6 ставится в один клик — в статье «Готовые рецепты VMmanager 6»; работа с файрволом подробнее разобрана в статье «Настройка firewall (ufw)».