2025, Nov 14 06:02

Почему FastAPI в Docker «слушает», но не принимает подключения

Почему FastAPI в Docker с Uvicorn на 0.0.0.0:8000 «слушает», но curl и Postman не подключаются: блокирующая инициализация transformers. Проверка и решение.

Docker‑приложение на FastAPI собирается и запускается, в логах видно, что Uvicorn слушает 0.0.0.0:8000, но curl или Postman не подключаются. Это частая ловушка: тяжёлая инициализация происходит во время импорта, и сервер так и не достигает состояния «готов принимать трафик». Ниже — практичный способ изолировать причину и выйти на надёжный путь к исправлению без угадываний.

Сценарий воспроизведения и пример, где всё ломается

Приложение предоставляет эндпойнт POST /classify и загружает zero‑shot pipeline из transformers на этапе импорта. Контейнер стартует с uvicorn, порт 8000 опубликован. Попытка подключения завершается ошибкой «Couldn’t connect to server».

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
from transformers import pipeline as hf_pipeline

class InPayload(BaseModel):
    text: str = Field(..., example="")
    lang: str = Field(
        "en",
        pattern=r"^(en|tr|de|fr|es|it|pt|ru|ar|zh|ja|ko|hi|bn|ur|fa|th|vi|id|ms|nl|sv|no|da|fi|pl|cs|sk|hu|ro|bg|hr|sr|sl|et|lv|lt|el|he|uk|be|ky|uz|km|my|tg|az|hy|ga|cy|is|mk|bs|sq|mn|ne|pa|gl|la)$",
        description="ISO language code",
        example="tr"
    )

class OutPayload(BaseModel):
    label: str
    score: float

api = FastAPI(title="Spam & Abuse Detector")

nlp = hf_pipeline(
    "zero-shot-classification",
    model="joeddav/xlm-roberta-large-xnli"
)

CHOICES = ["spam", "adult_content", "drugs", "non_spam"]

@api.post("/classify", response_model=OutPayload)
def detect(payload: InPayload):
    outcome = nlp(
        sequences=payload.text,
        candidate_labels=CHOICES
    )
    top_idx = outcome["scores"].index(max(outcome["scores"]))
    top_label = outcome["labels"][top_idx]
    top_score = outcome["scores"][top_idx]
    return OutPayload(label=top_label, score=top_score)

Что на самом деле происходит

Лог сервера обрывается после «Started reloader process …» и не печатает привычные строки «Started server process» и «Application startup complete». Разница принципиальна. Когда тяжёлая зависимость, например pipeline из transformers, создаётся при импорте, она блокирует запуск, и Uvicorn не доходит до этапа, когда начинает принимать подключения. В результате любые запросы из curl или Postman терпят неудачу, потому что сокет ещё не открыт.

Есть простой способ это проверить: временно заменить pipeline и возвращать фиктивные данные. Если соединения проходят и сервер доходит до «Application startup complete», значит, с контейнером и обвязкой FastAPI всё в порядке, а проблема находится именно в пути инициализации модели.

Рабочая изоляция: проверяем здоровье сервера без pipeline

Запустите приложение с тем же API, но без transformers pipeline. Так вы проверите Dockerfile, сеть и роутинг FastAPI от начала до конца.

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field

class InPayload(BaseModel):
    text: str = Field(..., example="")
    lang: str = Field(
        "en",
        pattern=r"^(en|tr|de|fr|es|it|pt|ru|ar|zh|ja|ko|hi|bn|ur|fa|th|vi|id|ms|nl|sv|no|da|fi|pl|cs|sk|hu|ro|bg|hr|sr|sl|et|lv|lt|el|he|uk|be|ky|uz|km|my|tg|az|hy|ga|cy|is|mk|bs|sq|mn|ne|pa|gl|la)$",
        description="ISO language code",
        example="tr"
    )

class OutPayload(BaseModel):
    label: str
    score: float

api = FastAPI(title="Spam & Abuse Detector")

@api.get("/")
def root():
    return {"Hello": "World"}

TAGS = ["spam", "adult_content", "drugs", "non_spam"]

@api.post("/classify", response_model=OutPayload)
def detect(payload: InPayload):
    fake_label = payload.text
    fake_score = float(len(payload.text))
    return OutPayload(label=fake_label, score=fake_score)

В этой версии сервер проходит полный цикл старта. Вы должны увидеть строки вроде:

INFO: Started server process [...]
INFO: Waiting for application startup.
INFO: Application startup complete.

Когда он поднялся, отправьте локальный запрос с той же машины, где запущен Docker:

curl -X POST http://127.0.0.1:8000/classify \
  -H "Content-Type: application/json" \
  -d '{"text":"bla bla","lang":"en"}'

Если в ответ приходит JSON, контейнер, приложение FastAPI и проброс портов работают корректно. Это изолирует проблему в инициализации модельного pipeline.

Что менять и как двигаться дальше

Здесь есть два практических вывода. Во‑первых, раз мок‑реализация принимает подключения, Dockerfile и связка приложения настроены верно. Во‑вторых, zero‑shot pipeline, создаваемый при импорте, — это блокирующая операция. На практике это означает: пока pipeline загружается, сервис не готов принимать запросы. Готовность можно проверять по строке «Application startup complete» в логах — и лишь после этого бить по эндпойнту. Если начать слать трафик раньше, подключения будут падать.

Ещё один источник путаницы — клиент. Если вы используете браузерный клиент, работающий удалённо, он не сможет достучаться до 127.0.0.1 на вашей машине. Используйте локальный curl или установленный локально клиент с адресом http://127.0.0.1:8000.

Почему это важно

Большие модели откладывают готовность приложения и легко маскируются под сетевые проблемы. Понимание последовательности запуска и умение изолировать тяжёлую инициализацию помогают не тратить время на неверный слой. Проверка приложения с лёгким заглушечным вариантом исключает Docker и роутинг из уравнения и фокусирует усилия на настоящем узком месте.

Выводы

Держите инициализацию под контролем и следите за логами. Если сервер не вывел «Application startup complete», он ещё не готов, и попытки подключения будут проваливаться. В диагностике сначала подставьте мок‑ответ, чтобы убедиться, что контейнер и FastAPI в порядке, затем верните модель и дождитесь её загрузки перед тестированием. А для клиента сперва предпочтите локальный curl — так проще исключить недоступность вашего localhost.