Гайды

Обратный прокси перед ИИ-сервисом: nginx или Caddy

Две настройки по умолчанию ломают именно ИИ-приложения: буферизация убивает потоковый ответ, короткий таймаут рвёт долгую генерацию. Готовые конфигурации для обоих серверов и способ проверить, что поток действительно течёт.

Обратный прокси перед обычным сайтом ставится по инструкции из четырёх строк, и она работает. Перед ИИ-приложением та же инструкция даёт сервис, который внешне работает, а на деле сломан в двух местах — и оба ломаются так, что виноватым выглядит не прокси.

Первое: ответ модели перестаёт быть потоковым. Вместо текста, появляющегося по мере генерации, пользователь тридцать секунд смотрит на индикатор, а потом получает всё сразу. Естественный вывод — «модель тормозит». Модель ни при чём: прокси копит ответ в буфере.

Второе: длинные ответы обрываются на середине. Тоже похоже на проблему модели или сети. И тоже нет: прокси разорвал соединение по таймауту чтения.

Ниже — обе настройки, полные конфигурации для nginx и Caddy и способ проверить, что поток действительно течёт.

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

  • Домен, указывающий на сервер. Про записи и типовые ошибки — в гайде про настройку DNS без боли.
  • Приложение, слушающее локальный адрес (обычно 127.0.0.1:8000), а не внешний. Прокси на то и ставят, чтобы наружу торчал только он.
  • Понимание, каким способом ваше приложение отдаёт поток: поток событий (Server-Sent Events), фрагменты по протоколу, или веб-сокет. От этого зависит часть настроек.

#Шаг 1. Отключить буферизацию — иначе потока нет

Прокси по умолчанию буферизует ответ бэкенда: читает целиком, потом отдаёт клиенту. Для страницы это разумно, для потока — фатально.

location / {
    proxy_pass http://127.0.0.1:8000;
    proxy_http_version 1.1;
    proxy_set_header Connection "";      # держим соединение открытым
    proxy_buffering off;                  # главное
    proxy_cache off;
}

proxy_http_version 1.1 вместе с пустым Connection обязательны: по умолчанию к бэкенду идёт версия 1.0 без постоянного соединения, а на ней потока не будет независимо от остальных настроек.

Существует и второй путь — заголовок X-Accel-Buffering: no, который приложение выставляет само на потоковых ответах. Он точнее, потому что не отключает буферизацию для всего остального. Здоровая схема — оба: заголовок в приложении и proxy_buffering off на потоковых маршрутах.

В Caddy буферизация по умолчанию выключена, и это его главное практическое преимущество в нашем сценарии: типовая ошибка просто не совершается.

#Шаг 2. Поднять таймауты под длинную генерацию

Значение по умолчанию у nginx — шестьдесят секунд между чтениями. Обычному сайту хватает с запасом. Локальная модель на холодном старте может думать дольше: сначала грузятся веса, потом идёт обработка запроса, и только потом появляется первый токен.

proxy_connect_timeout 10s;    # соединиться с бэкендом — быстро или никогда
proxy_send_timeout   600s;    # отправка запроса (большие вложения)
proxy_read_timeout   600s;    # ожидание данных от бэкенда
send_timeout         600s;    # отправка клиенту

Обратите внимание на разницу: proxy_connect_timeout не надо делать большим. Если бэкенд не принял соединение за десять секунд, он лежит, и ожидание минуты ничего не улучшит — только удлинит очередь.

Важная деталь про proxy_read_timeout: это таймаут между порциями данных, а не на весь ответ. Поэтому у потокового ответа, который стабильно шлёт токены, он практически не срабатывает. Он срабатывает ровно в двух местах: пока модель думает до первого токена и когда поток событий молчит между сообщениями. Второй случай лечится не таймаутом, а сердцебиением: приложение раз в 15–30 секунд шлёт пустой комментарий в поток, и все промежуточные узлы видят живое соединение.

#Шаг 3. Полные конфигурации

#nginx

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

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    client_max_body_size 64m;         # голосовые и вложения

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        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_buffering off;
        proxy_cache off;
        proxy_connect_timeout 10s;
        proxy_read_timeout   600s;
        proxy_send_timeout   600s;
    }

    location /ws/ {                    # если есть веб-сокеты
        proxy_pass http://127.0.0.1:8000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade    $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 3600s;
    }
}

server {
    listen 80;
    server_name example.com;
    return 301 https://$host$request_uri;
}

#Caddy

example.com {
    encode gzip

    reverse_proxy 127.0.0.1:8000 {
        flush_interval -1          # немедленная отдача: поток не копится
        transport http {
            read_timeout    600s
            write_timeout   600s
            dial_timeout     10s
        }
    }

    request_body {
        max_size 64MB
    }
}

Caddy короче не потому, что он «проще», а потому, что делает за вас две вещи: сам получает и продлевает сертификат и по умолчанию не буферизует. flush_interval -1 явно закрепляет второе — стоит написать, даже если это значение по умолчанию: конфигурация должна выражать намерение, а не полагаться на него.

#Шаг 4. Передать приложению правду о схеме

X-Forwarded-Proto в списке заголовков — не формальность. За прокси приложение видит обычный http:// и на этом основании принимает решения: не ставит Strict-Transport-Security, генерирует ссылки с неверной схемой, помечает куки как небезопасные. Симптом обычно один и сбивающий с толку: «после входа сразу разлогинивает».

Обратная сторона: приложение должно доверять этому заголовку только от вашего прокси. Заголовок, пришедший снаружи, — это подделка. Ограничьте доверенные адреса в настройках приложения.

Здесь же решается, где живут заголовки безопасности. Правило: в одном месте. Политика безопасности контента, выставленная и в приложении, и в прокси, применяется браузером как пересечение двух — и результат оказывается строже каждой из них по отдельности. Классический симптом: локально всё работает, за прокси половина интерфейса не грузится. Подробности — в гайде про Content Security Policy.

#Проверка результата

Проверять надо не «сайт открылся», а три конкретных свойства.

# 1. поток действительно течёт: строки должны появляться по одной,
#    а не вывалиться разом в конце
curl -N -s https://example.com/api/chat/stream -H 'Accept: text/event-stream'

# 2. буферизация не включена где-то ещё
curl -sI https://example.com/api/chat/stream | grep -i -E 'x-accel-buffering|content-length'
# у потокового ответа Content-Length быть НЕ должно

# 3. приложение знает, что пришло по HTTPS
curl -sI https://example.com/ | grep -i strict-transport-security

Ключ -N в первой команде отключает буферизацию на стороне curl — без него вы будете проверять curl, а не прокси. Именно на этом чаще всего и ошибаются: «поток не работает» оказывается свойством инструмента проверки.

Критерий приёмки для первой команды — строки идут с той же скоростью, с какой их отдаёт приложение при обращении напрямую по локальному адресу. Сравните оба вызова подряд, это занимает минуту.

#Что может пойти не так

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

Ответ обрывается ровно через 60 секунд. Таймаут чтения не поднят или поднят не в том блоке location. Проверьте, что настройка попала именно в тот маршрут, который отдаёт поток.

Появились ошибки 502 под нагрузкой. Обычно упирается не прокси, а бэкенд: закончились рабочие процессы. Прокси здесь только гонец.

Загрузка файла обрывается. client_max_body_size по умолчанию — единицы мегабайт. Голосовое сообщение или архив в него не помещаются, и отказ приходит от прокси, а не от приложения.

После настройки прокси приложение перестало видеть адреса клиентов. Все запросы приходят с локального адреса. Нужны X-Real-IP и X-Forwarded-For плюс доверие к ним на стороне приложения.

Сертификат не выпустился. У nginx выпуск и продление — отдельная задача, у Caddy — встроенная; в обоих случаях ломается обычно продление, а не первичная выдача. Разбор — в гайде про HTTPS и автопродление сертификатов.

#Частые вопросы

#nginx или Caddy — что выбрать под ИИ-приложение?

Caddy, если вы ставите прокси впервые и вам нужен один сервис: он сам управляет сертификатами и по умолчанию не буферизует, то есть обе главные ловушки просто не возникают. nginx, если у вас уже есть на нём инфраструктура, нужна тонкая настройка кэширования или ограничение частоты запросов — там его возможности богаче.

#Нужен ли прокси вообще, если приложение умеет HTTPS само?

Полезен по трём причинам: он терминирует TLS в одном месте, позволяет держать несколько сервисов на одном адресе и даёт ограничение частоты запросов и размера тела без правки приложения. Для одного сервиса на выделенной машине можно обойтись и без него.

#Почему ответ приходит целиком, хотя буферизация выключена?

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

#Как быть с веб-сокетами?

Им нужен отдельный блок с передачей заголовков обновления протокола и заметно большим таймаутом чтения — соединение живёт часами. Обратите внимание: политика безопасности контента различает https: и wss:, и разрешение для первого не покрывает второе.

#Какой таймаут ставить, если модель локальная и медленная?

Считайте по худшему случаю: холодный старт модели плюс самый длинный ответ, который вы допускаете. Затем добавьте запас. И параллельно уберите сам худший случай — держите модель прогретой, чтобы таймаут никогда не проверялся на практике.

#Куда прокси встраивается в общую картину?

Это пятый этап из шести в маршруте о том, как поднять свой ИИ-сервис: после домена и сертификата, до эксплуатации. Проверки прокси и приложения при этом должны быть раздельными, иначе одна тревога будет означать два разных отказа — про это в статье про мониторинг личного сервиса.

Хотите ИИ, который помнит вас?

Persona держит контекст между чатами и работает на вашем железе — если развернуть её у себя и подключить локальную модель.

Создать аккаунт