Обратный прокси: proxy_pass и поток запроса

proxy_pass — это волшебное слово, превращающее Nginx из раздатчика файлов в посредника между интернетом и твоим приложением.
«Клиент думает, что говорит с Nginx. Бэкенд думает, что говорит с Nginx. А Nginx — посередине, и оба счастливы.»

Обратный прокси — главная роль Nginx в современном вебе. Идея проста: Nginx принимает запрос от клиента и пересылает его внутреннему приложению (бэкенду), а ответ бэкенда возвращает клиенту. Директива, которая это включает, — proxy_pass.

Базовый проксирующий конфиг

server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://127.0.0.1:8000;   # приложение на localhost:8000
        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;
    }
}

Здесь Nginx слушает порт 80, а всё, что приходит на /, отправляет приложению на 127.0.0.1:8000. Приложение слушает только localhost — снаружи оно недоступно.

Поток запроса

   Клиент            Nginx (:80)           Бэкенд (:8000)
     |                  |                      |
     |  GET / ------->  |                      |
     |                  |  GET / + заголовки -->|
     |                  |                      | (генерирует ответ)
     |                  |  <-- 200 OK ----------|
     |  <-- 200 OK -----|                      |
     |                  |                      |

Клиент общается только с Nginx и не подозревает о существовании бэкенда. Бэкенд видит запрос от Nginx. Заголовки X-Real-IP и X-Forwarded-For нужны, чтобы приложение всё-таки узнало реальный IP клиента — иначе оно увидит лишь IP самого Nginx.

Коварный слеш в proxy_pass

# БЕЗ слеша: префикс location СОХРАНЯЕТСЯ
location /api/ {
    proxy_pass http://127.0.0.1:8000;
}
# /api/users  ->  бэкенд получит  /api/users

# СО слешем: префикс location ЗАМЕНЯЕТСЯ
location /api/ {
    proxy_pass http://127.0.0.1:8000/;
}
# /api/users  ->  бэкенд получит  /users  (без /api)

Как работает под капотом

Получив запрос, Nginx устанавливает новое соединение с бэкендом и пересылает запрос с теми заголовками, что ты задал через proxy_set_header. Ответ бэкенда буферизуется (по умолчанию proxy_buffering on): Nginx может принять ответ целиком в буфер и отдавать клиенту в его темпе, освобождая бэкенд. Так медленный клиент не держит ценный воркер приложения занятым — Nginx «впитывает» ответ и кормит клиента сам.

Частые ошибки

  • Забыть proxy_set_header Host $host;. Многие приложения по Host строят ссылки и выбирают сайт; без него получишь странности.
  • Перепутать слеш в proxy_pass. Лишний или недостающий / ломает путь, и бэкенд отдаёт 404.
  • Не передать реальный IP. Без X-Forwarded-For приложение залогирует IP Nginx, а не пользователя.
  • Бэкенд слушает 0.0.0.0. Тогда его можно дёрнуть напрямую в обход Nginx — пусть слушает только 127.0.0.1.

Best practices

  • Всегда задавай связку Host, X-Real-IP, X-Forwarded-For, X-Forwarded-Proto.
  • Вынеси эти proxy_set_header в snippet и подключай через include.
  • Бэкенд — только на 127.0.0.1; наружу смотрит лишь Nginx.

Почему буферизация — это не мелочь

Может показаться, что буферизация ответа — деталь, но именно она часто определяет, сколько пользователей выдержит твой бэкенд. Представь приложение с дорогим воркером (например, Python с GIL или PHP-FPM с ограниченным пулом). Без Nginx такой воркер, отдав ответ, вынужден ждать, пока медленный мобильный клиент дочитает его байт за байтом — и всё это время воркер занят и недоступен другим. С буферизацией Nginx мгновенно «впитывает» весь ответ в свою память, освобождает воркер бэкенда, и уже сам, своими дешёвыми событийными воркерами, докармливает ответ медленному клиенту. Один и тот же бэкенд за Nginx обслуживает в разы больше запросов в секунду.

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

Итоги

Обратный прокси через proxy_pass делает Nginx посредником: клиент общается с Nginx, Nginx — с бэкендом. Заголовки X-Forwarded-* доносят до приложения реальные данные клиента, а буферизация защищает бэкенд от медленных клиентов. Слеш в proxy_pass решает, сохранять ли префикс пути. Дальше — тонкая настройка проксирования: таймауты, буферы, WebSocket.

Проверьте себя
1. Что делает директива proxy_pass?
AОтдаёт файл с диска
BПересылает запрос на указанный бэкенд и возвращает клиенту его ответ
CСжимает ответ
DШифрует соединение с клиентом
2. Чем отличается `proxy_pass http://app:8000;` от `proxy_pass http://app:8000/;` для location /api/?
AНичем
BБез слеша префикс /api/ сохраняется и уходит на бэкенд, со слешем он заменяется (отрезается)
CСлеш включает HTTPS
DСлеш отключает буферизацию