Обратный прокси перед обычным сайтом ставится по инструкции из четырёх строк, и она работает. Перед ИИ-приложением та же инструкция даёт сервис, который внешне работает, а на деле сломан в двух местах — и оба ломаются так, что виноватым выглядит не прокси.
Первое: ответ модели перестаёт быть потоковым. Вместо текста, появляющегося по мере генерации, пользователь тридцать секунд смотрит на индикатор, а потом получает всё сразу. Естественный вывод — «модель тормозит». Модель ни при чём: прокси копит ответ в буфере.
Второе: длинные ответы обрываются на середине. Тоже похоже на проблему модели или сети. И тоже нет: прокси разорвал соединение по таймауту чтения.
Ниже — обе настройки, полные конфигурации для 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:, и разрешение для первого не покрывает второе.
#Какой таймаут ставить, если модель локальная и медленная?
Считайте по худшему случаю: холодный старт модели плюс самый длинный ответ, который вы допускаете. Затем добавьте запас. И параллельно уберите сам худший случай — держите модель прогретой, чтобы таймаут никогда не проверялся на практике.
#Куда прокси встраивается в общую картину?
Это пятый этап из шести в маршруте о том, как поднять свой ИИ-сервис: после домена и сертификата, до эксплуатации. Проверки прокси и приложения при этом должны быть раздельными, иначе одна тревога будет означать два разных отказа — про это в статье про мониторинг личного сервиса.