В 2026 году главная роль Nginx - не раздавать статику (хотя он это умеет блестяще), а стоять на входе и разруливать трафик к бэкендам. Это и называется nginx reverse proxy, обратный прокси. Клиент стучится в один публичный адрес, видит один TLS-сертификат, один домен - а за спиной Nginx сидит зоопарк: PHP-FPM, Node, Go-сервис, питоновский gunicorn, ещё один Nginx. Прямой прокси (forward) работает за клиента и ходит в интернет от его имени. Обратный (reverse) работает за сервер: принимает запрос снаружи и решает, какому внутреннему апстриму его отдать. С точки зрения клиента бэкенда вообще не существует - есть только шлюз.
Боль, которую это решает, простая. Бэкенд не должен торчать наружу: у него нет нормального TLS, он не умеет rate limit, он падает под нагрузкой соединений, он не знает про твой домен. Nginx берёт на себя терминацию TLS, буферизацию медленных клиентов, балансировку, кэш, лимиты - а бэкенду отдаёт чистый, быстрый, предсказуемый HTTP. Вся эта магия начинается с одной директивы - nginx proxy_pass. Но именно вокруг неё больше всего граблей, потому что её поведение зависит от одного символа в конце строки. Разберём механику до винтика.

nginx proxy_pass: слеш в конце меняет всё
Директива proxy_pass говорит: "этот запрос отдай туда". Пишется внутри location. И вот тут ключевое правило, которое ломает людям прод годами: есть ли в proxy_pass URI-часть (путь после хоста) или нет.
Случай первый - в proxy_pass нет пути, только схема и адрес:
Код: Выделить всё
location /api/ {
proxy_pass http://127.0.0.1:9000;
}
Случай второй - в proxy_pass есть путь (даже если это просто слеш):
Код: Выделить всё
location /api/ {
proxy_pass http://127.0.0.1:9000/;
}
Сравни на конкретике, запрос всегда GET /api/users/42:
- proxy_pass http://up; (без пути) -> бэкенд видит /api/users/42
- proxy_pass http://up/; (слеш) -> бэкенд видит /users/42
- proxy_pass http://up/v2/; -> бэкенд видит /v2/users/42
- proxy_pass http://up/v2; (без слеша на конце, с путём) -> бэкенд видит /v2users/42 - склейка без разделителя, классический баг
Важная оговорка: режим с отрезанием префикса не работает, если внутри location есть rewrite, который меняет $uri, или если location задан регэкспом (location ~). В этих случаях Nginx физически не знает, какую часть отрезать, и URI-часть в proxy_pass запрещена - надо передавать переменную или собирать путь самому. Это, кстати, ещё одна причина в 2026 минимизировать rewrite-правила: помимо граблей с proxy_pass, в ngx_http_rewrite_module нашли CVE-2026-42945 (heap overflow, "NGINX Rift"), так что лишний rewrite - это и риск, и путаница. Меньше переписывания пути - проще жизнь.
nginx проброс заголовков: что бэкенд должен узнать о клиенте
Как только запрос ушёл через прокси, бэкенд теряет связь с реальностью. Он видит соединение от Nginx, а не от клиента. Значит, $remote_addr на бэкенде - это IP Nginx. Host в запросе по умолчанию подменяется на адрес из proxy_pass. Схема (http/https) теряется - до бэкенда всё доходит как plain http, ведь TLS терминировал Nginx. Если бэкенду это важно (а ему почти всегда важно - для логов, редиректов, генерации абсолютных URL, rate limit, авторизации по IP) - мы обязаны рассказать ему правду заголовками. Это и есть nginx проброс заголовков.
Базовый джентльменский набор, как в global_proxy эталонного gateway:
Код: Выделить всё
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_set_header X-Forwarded-Host $host;
nginx proxy_set_header Host $host - самое частое. Без него Nginx подставит в Host адрес апстрима (127.0.0.1:9000), и приложение с виртуальными хостами растеряется: куда пришли, на какой домен? $host - это имя из строки запроса или заголовка Host клиента. Вариант $http_host передаёт заголовок ровно как прислал клиент (с портом), $host - нормализованный. Для большинства задач $host - правильный выбор.
X-Real-IP $remote_addr - кладём настоящий IP клиента (на входном Nginx $remote_addr ещё настоящий) в отдельный заголовок. Бэкенд читает его и знает, кто пришёл.
X-Forwarded-For $proxy_add_x_forwarded_for - это умная переменная. Она берёт пришедший X-Forwarded-For и дописывает в конец текущий $remote_addr через запятую. Получается цепочка хопов: client, proxy1, proxy2. Если поставить просто $remote_addr - затрёшь историю при многоуровневом проксировании.
X-Forwarded-Proto $scheme - сообщает бэкенду исходную схему. Nginx терминировал TLS, к бэкенду идёт http, но клиент-то пришёл по https. Без этого заголовка фреймворк (Laravel, Django, Rails) сгенерирует http-ссылки и редиректы, словит mixed content и циклы редиректов. $scheme = http или https в зависимости от того, как клиент достучался до Nginx.
Критично про безопасность: эти заголовки имеют смысл, только если на самом входном прокси. Если твой Nginx стоит за CDN или другим прокси, $remote_addr - это уже не клиент, а соседний прокси. Тогда нужен модуль realip (set_real_ip_from на доверенные сети + real_ip_header X-Forwarded-For + real_ip_recursive on), иначе и X-Real-IP, и rate limit, и гео будут считать по чужому IP. И никогда не доверяй X-Forwarded-For от кого попало: клиент может прислать его сам и подделать свой IP. Доверяй только заголовкам от известных сетей прокси/CDN.
2026: keepalive к бэкенду теперь по умолчанию
Тут важное обновление, которое многие проспали. Раньше (до nginx 1.29.7) соединение от Nginx к бэкенду по умолчанию шло по HTTP/1.0 с Connection: close - то есть на каждый запрос новый TCP-коннект к апстриму, открыли-закрыли. Под нагрузкой это жрало порты, грело CPU на хендшейках и упиралось в TIME_WAIT. Чтобы включить переиспользование соединений, приходилось руками писать три вещи: keepalive в блоке upstream, proxy_http_version 1.1 и обнуление заголовка Connection.
С nginx 1.29.7 (вошло в stable 1.30.0, апрель 2026) поведение по умолчанию изменилось: соединение к бэкенду теперь HTTP/1.1 с включённым keepalive. По умолчанию кэшируется до 32 соединений на каждый воркер-процесс, и эти соединения не шарятся между разными location. Это заметно снижает накладные расходы на работу с бэкендами из коробки.
Но - и это важно для практики - ты всё равно должен держать в конфиге явную настройку. Причины две. Первая: ты не знаешь, на какой версии Nginx это поедет в проде или у коллеги - на 1.28 дефолт ещё старый. Вторая: дефолтные 32 коннекта на воркер часто маловато для нагруженного API. Поэтому канонический явный рецепт, который работает везде и однозначен:
Код: Выделить всё
upstream backend {
server 10.0.0.1:9000 max_fails=3 fail_timeout=30s;
server 10.0.0.2:9000 max_fails=3 fail_timeout=30s;
keepalive 64;
}
server {
location /api/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
Отдельно про новый параметр keepalive ... local (появился вместе с этим изменением) - он управляет привязкой кэшированных соединений к локальному адресу. На обычном шлюзе не нужен, всплывает при хитрой маршрутизации с несколькими исходящими интерфейсами.
[p][/p]Буферизация, proxy_redirect и таймауты - управляем потоком
Nginx по умолчанию буферизует ответ бэкенда: proxy_buffering on. Что это значит на практике. Бэкенд (медленный PHP-FPM) выплёвывает ответ Nginx максимально быстро, Nginx складывает его в буферы (в память, при переполнении - во временный файл) и освобождает бэкенд. Дальше Nginx уже сам, не торопясь, отдаёт ответ медленному мобильному клиенту на 3G. Бэкенд не держит дорогой воркер всё время, пока клиент качает - это спасает пул процессов приложения. Настройки буферов из эталона:
Код: Выделить всё
proxy_buffering on;
proxy_buffer_size 16k;
proxy_buffers 16 16k;
proxy_busy_buffers_size 32k;
Когда буферизацию выключать. Есть сценарии, где буфер - враг: стриминг, Server-Sent Events (SSE), long-polling, прогресс-бары, чат на потоке токенов от LLM. Там клиент должен получать данные сразу, байт за байтом, а не ждать, пока Nginx накопит буфер. Для таких location:
Код: Выделить всё
location /stream/ {
proxy_pass http://backend;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 1h;
}
proxy_request_buffering - зеркальная штука, но про тело запроса, не ответа. По умолчанию on: Nginx сначала целиком вычитывает тело запроса от клиента (например, загрузку файла) и только потом начинает слать бэкенду. Это защищает бэкенд от медленных клиентов (slowloris на upload). Но при загрузке гигабайтных файлов это значит, что Nginx буферизует весь файл (в память/на диск) перед отправкой. Для больших стриминговых загрузок выключают: proxy_request_buffering off - тогда Nginx стримит тело бэкенду по мере получения. Решай по задаче.
proxy_redirect - чинит заголовок Location в ответе. Бэкенд за прокси не знает внешнего адреса и может прислать редирект на свой внутренний http://127.0.0.1:9000/login. Клиент по такому уйдёт в никуда. proxy_redirect переписывает Location и Refresh в ответе:
Код: Выделить всё
proxy_redirect http://127.0.0.1:9000/ https://api.example.com/;
Таймауты - последний рубеж. Из эталона:
Код: Выделить всё
proxy_connect_timeout 5s;
proxy_send_timeout 10s;
proxy_read_timeout 10s;
proxy_next_upstream error timeout;
proxy_next_upstream_tries 3;
Типичные грабли nginx прокси
- Слеш в proxy_pass. Самая частая боль. /api/ -> proxy_pass http://up; отдаёт /api/users, а proxy_pass http://up/; отдаёт /users. Перепутал - получил 404 или дубль префикса. Всегда сверяй слеши location и proxy_pass.
- Забыл Host $host. Бэкенд с виртуалхостами отдаёт дефолтный сайт или 404, потому что в Host прилетел 127.0.0.1.
- Забыл X-Forwarded-Proto. Бесконечные редиректы https -> http -> https, mixed content, сломанная генерация ссылок во фреймворке.
- Доверие к X-Forwarded-For без realip. Клиент подделывает IP, твой rate limit и бан по IP обходятся в один заголовок.
- Capture-группы и переменные в proxy_pass. Как только в proxy_pass появляется переменная (proxy_pass http://$backend;), Nginx начинает резолвить имя через resolver на каждый запрос и НЕ использует кэш upstream - нужен resolver в конфиге, иначе ошибка. Это другой режим работы, осторожно.
- proxy_buffering off везде "для скорости". Миф. Для обычного API буферизация ускоряет (освобождает бэкенд), выключать её надо только для стримов.
- rewrite ломает отрезание префикса. Если в location есть rewrite, меняющий $uri, режим "отрезать префикс" в proxy_pass перестаёт работать - URI пойдёт уже переписанный.
Повторяем по шагам, нужен любой Linux с nginx и питоном.
- Шаг 1. Подними фейковый бэкенд, который покажет, что до него дошло. В отдельном терминале запусти простой эхо-сервер - подойдёт даже (он логирует пришедший путь в консоль) либо мини-приложение на flask, печатающее заголовки.
Код: Выделить всё
python3 -m http.server 9000 - Шаг 2. Создай конфиг proxy.conf:
Код: Выделить всё
server { listen 8081; location /api/ { proxy_pass http://127.0.0.1:9000/; 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_http_version 1.1; proxy_set_header Connection ""; } } - Шаг 3. Проверь синтаксис и перечитай конфиг:
Код: Выделить всё
nginx -t && nginx -s reload - Шаг 4. Дёрни прокси: . Смотри в лог бэкенда - он должен показать запрос /users/42 (префикс /api/ отрезан слешем).
Код: Выделить всё
curl -i http://127.0.0.1:8081/api/users/42 - Шаг 5. Убери слеш в proxy_pass (стало http://127.0.0.1:9000;), reload, повтори curl. Теперь бэкенд увидит /api/users/42. Ты только что руками воспроизвёл правило про слеш.
- Шаг 6. На бэкенде распечатай пришедшие заголовки и убедись, что в них есть Host, X-Real-IP, X-Forwarded-For, X-Forwarded-Proto с твоим IP.
- Шаг 7. Включи в location и сравни поведение на стрим-эндпоинте (если есть). На обычном ответе разницы не заметишь - и это нормально.
Код: Выделить всё
proxy_buffering off;
- location /app/, запрос /app/css/main.css. Что увидит бэкенд при proxy_pass http://up; и что при proxy_pass http://up/static/; ?
- Зачем нужен X-Forwarded-Proto $scheme и что сломается во фреймворке, если его не передать?
- Что изменилось в nginx 1.29.7 / 1.30 в дефолтном соединении к бэкенду и зачем тогда всё равно писать proxy_http_version 1.1 и Connection ""?
- В каком сценарии proxy_buffering надо выключить и какую цену за это платит бэкенд?
Обратный прокси - это сердце Nginx 2026. Директива proxy_pass решает, куда уйдёт запрос, а один слеш в её конце решает, с каким путём он туда придёт - запомни правило про префикс намертво. Бэкенд за прокси слеп: расскажи ему правду через proxy_set_header Host $host, X-Real-IP, X-Forwarded-For и X-Forwarded-Proto, и не доверяй этим заголовкам без realip. С версии 1.30 keepalive к бэкенду включён по умолчанию, но явные proxy_http_version 1.1 и пустой Connection оставляем для совместимости и контроля пула. Буферизацию держим включённой для обычного API и выключаем точечно для стримов и SSE. Таймауты - твой предохранитель от зависших бэкендов. Это фундамент, на котором дальше вырастут балансировка, кэш и полноценный API-gateway.