FastAPI создан для того, чтобы быстро писать и быстро работать, поэтому его всё чаще выбирают для API мобильных приложений, Telegram-ботов и внутренних сервисов. Локально всё запускается за секунду командой uvicorn main:app --reload. Но этот режим предназначен для разработки: --reload следит за файлами, работает в одном процессе и не восстанавливается сам после перезагрузки сервера. Разберём, как правильно разместить FastAPI на VPS, превратить его в постоянно работающий сервис и открыть через Nginx с HTTPS.
Что такое ASGI и зачем нужен Uvicorn
FastAPI основан на стандарте ASGI, то есть поддерживает асинхронные запросы, WebSocket-соединения и долгоживущие подключения. Чтобы запустить такое приложение, нужен ASGI-сервер, и самый распространённый из них — Uvicorn. Он может запускать несколько рабочих процессов, а каждый процесс параллельно обрабатывает множество асинхронных запросов. Nginx стоит впереди и берёт на себя HTTPS-шифрование, медленных клиентов и ограничения на размер запросов, так что Python-процессы заняты только логикой приложения.
Пример: ташкентская чайхана с национальной кухней заказала мобильное приложение для доставки. Приложение обращается к сервису FastAPI по адресу api.misol.uz: получает меню, отправляет заказы и через WebSocket следит за местоположением курьера. В обеденное время число запросов резко растёт, поэтому сервис должен работать с несколькими воркерами, автоматически восстанавливаться и стоять за правильно настроенным прокси.
Подготовка проекта на сервере
Создайте для приложения отдельного системного пользователя и виртуальное окружение. Зависимости ставьте из requirements.txt; вариант Uvicorn standard подтягивает более быстрый цикл событий и библиотеки для WebSocket.
sudo apt update
sudo apt install python3-venv nginx
sudo adduser --system --group --home /srv/api api
sudo -u api python3 -m venv /srv/api/venv
sudo -u api /srv/api/venv/bin/pip install -r /srv/api/requirements.txt "uvicorn[standard]"
Пароль к базе, ключ JWT и токены внешних сервисов хранятся не в коде, а в файле /srv/api/.env, и приложение читает их как переменные окружения. Поставьте на файл chmod 600, чтобы доступ к нему был только у пользователя api. Сначала запустите приложение вручную и проверьте, что оно отвечает:
cd /srv/api
sudo -u api venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8000
curl http://127.0.0.1:8000/health
Настройки приложения для продакшена
По умолчанию FastAPI открыто показывает полное описание API на страницах /docs и /redoc. Для внутреннего или коммерческого API это лишняя информация, поэтому в продакшене их лучше отключить или открыть только для определённых IP-адресов. Для мониторинга добавьте простую точку проверки здоровья.
import os
from fastapi import FastAPI
is_prod = os.getenv("APP_ENV") == "production"
app = FastAPI(
docs_url=None if is_prod else "/docs",
redoc_url=None if is_prod else "/redoc",
)
@app.get("/health")
def health():
return {"status": "ok"}
Ещё один важный момент: если внутри эндпоинта, объявленного через async def, выполняется блокирующий код — синхронный HTTP-запрос или синхронный драйвер базы данных, — весь рабочий процесс ждёт, и остальные запросы тоже замедляются. В таких случаях либо используйте асинхронную библиотеку, либо объявите эндпоинт обычным def: тогда FastAPI выполнит его в отдельном пуле потоков.
Uvicorn как сервис systemd
Сервис должен автоматически подниматься после перезагрузки и перезапускаться после падения. Создайте файл /etc/systemd/system/api.service:
[Unit]
Description=FastAPI (Uvicorn) for api.misol.uz
After=network.target
[Service]
User=api
Group=api
WorkingDirectory=/srv/api
EnvironmentFile=/srv/api/.env
ExecStart=/srv/api/venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8000 --workers 2 --proxy-headers --forwarded-allow-ips=127.0.0.1
Restart=on-failure
[Install]
WantedBy=multi-user.target
--host 127.0.0.1 делает сервис доступным только изнутри сервера, снаружи к нему можно попасть лишь через Nginx. --proxy-headers и --forwarded-allow-ips велят доверять заголовкам X-Forwarded-For и X-Forwarded-Proto от Nginx, и приложение видит реальный IP клиента и то, что соединение идёт по HTTPS. Число воркеров подбирается по количеству ядер процессора. Включите сервис:
sudo systemctl daemon-reload
sudo systemctl enable --now api
sudo journalctl -u api -f
Прокси Nginx и WebSocket
WebSocket-соединение начинается с обычного HTTP-запроса с обновлением протокола, поэтому Nginx должен пропускать заголовки Upgrade и Connection. Сначала в файле /etc/nginx/conf.d/upgrade.conf объявляется вспомогательная переменная:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
Затем пишется конфигурация сайта /etc/nginx/sites-available/api:
server {
listen 80;
server_name api.misol.uz;
client_max_body_size 10M;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 300s;
}
}
sudo ln -s /etc/nginx/sites-available/api /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d api.misol.uz
proxy_read_timeout не даёт Nginx обрывать через минуту долгоживущие WebSocket-соединения вроде отслеживания курьера. Перед получением сертификата A-запись поддомена api должна указывать на адрес VPS; добавить её можно в разделе DNS кабинета sayt.uz.
Обновления и поиск проблем
Выпуск новой версии выглядит просто: обновить код, поставить зависимости, при необходимости выполнить миграции базы и запустить sudo systemctl restart api. Если Nginx возвращает 502, сначала загляните в логи journalctl -u api: чаще всего неверен путь к модулю, в .env не хватает переменной или порт занят другим процессом. Если внутри приложения IP клиента всегда выглядит как 127.0.0.1, проверьте параметр --proxy-headers. В примере с чайханой такой порядок позволяет безопасно обновляться даже перед обедом: сервис поднимается за несколько секунд, и мобильное приложение этого почти не замечает.