Три часа ночи, в чате паника, мониторинг красный, а пользователи видят белую страницу с надписью "502 Bad Gateway". Знакомо? Самая частая ошибка новичка в этот момент - начать крутить рандомные директивы и перезапускать nginx, надеясь, что само рассосётся. Не рассосётся. nginx troubleshooting - это не угадайка, а холодный детектив по логам.
Первое, что надо понять про любую ошибку шлюза: где она родилась. nginx стоит посередине - между клиентом и бэкендом. Ошибку может вернуть сам nginx (не дошло до бэкенда, права, лимиты), а может проксировать ответ бэкенда насквозь. Это два совершенно разных мира лечения, и путать их - терять часы.
Грубое правило, которое экономит время:
- 502 / 504 - почти всегда nginx не смог нормально поговорить с апстримом. Смотри бэкенд и сеть между ними.
- 403 / 404 / 413 - чаще всего сам nginx отказал ещё до апстрима: права, конфиг location, лимиты тела.
- 400 / 421 / 426 - проблема на уровне самого протокола/запроса, до маршрутизации.

Чтение error_log: одна строка - вся история
access_log говорит что вернули клиенту. error_log говорит почему. При траблшутинге твой главный файл - именно error_log. Открой реальную строку 502:
Код: Выделить всё
2026/06/17 03:14:22 [error] 1187#1187: *4521 connect() failed
(111: Connection refused) while connecting to upstream,
client: 203.0.113.7, server: api.example.com,
request: "GET /v1/orders HTTP/1.1",
upstream: "http://127.0.0.1:9000/v1/orders",
host: "api.example.com"
- [error] - уровень. Бывает debug, info, notice, warn, error, crit, alert, emerg.
- *4521 - номер соединения. По нему можно grep-нуть всю жизнь запроса в debug-логе.
- connect() failed (111: Connection refused) - ВОТ причина. Системный вызов connect() к апстриму вернул errno 111. Порт никто не слушает.
- while connecting to upstream - стадия. nginx ещё только устанавливал соединение.
- upstream: "http://127.0.0.1:9000/..." - КУДА он стучался. Сверь с тем, где реально слушает бэкенд.
- connect() failed (111: Connection refused) - бэкенд не слушает этот адрес/порт. Лежит или слушает другое.
- upstream timed out (110: Connection timed out) while reading response header - бэкенд жив, но думает дольше, чем proxy_read_timeout. Это дорога к 504.
- connect() to unix:/run/php-fpm.sock failed (13: Permission denied) - права на сокет или SELinux.
- no live upstreams while connecting to upstream - все серверы апстрима помечены недоступными после max_fails.
- open() "/var/www/html/img/x.png" failed (13: Permission denied) - 403, права на файл/каталог.
- open() "..." failed (2: No such file or directory) - 404, неверный root/alias/try_files.
Уровни логов и debug-лог: когда обычного error_log мало
По умолчанию error_log пишет с уровня error (плюс всё, что серьёзнее). Этого хватает для большинства аварий. Но иногда причина тонкая - например, "почему вообще выбрался этот location" или "почему отвалилось keepalive-соединение к апстриму". Тогда нужен debug-лог - он печатает каждый шаг обработки: разбор запроса, выбор location, фазы rewrite, открытие файлов, диалог с апстримом.
Включается так:
Код: Выделить всё
error_log /var/log/nginx/error.log debug;
Код: Выделить всё
nginx -V 2>&1 | tr ' ' '\n' | grep -- --with-debug
debug-лог огромный, поэтому не включай его глобально на проде. Сужай по клиенту - это спасает диск и нервы:
Код: Выделить всё
events {
debug_connection 203.0.113.7;
debug_connection 10.0.0.0/8;
}
Перед тем как лезть в debug, всегда прогони два базовых заклинания:
Код: Выделить всё
nginx -t # синтаксис + базовая валидация конфига
nginx -T # то же + дамп ВСЕГО итогового конфига со всеми include
Систематика по кодам: компендиум симптом -> причина -> лечение
Теперь главное - разбор кодов как FAQ. Держи это под рукой как чек-лист.
nginx 502 bad gateway - "бэкенд ответил мусором или не ответил вовсе".
nginx установил (или попытался) соединение, но не получил валидный HTTP-ответ. Причины по частоте:
- Бэкенд лёг/упал/не запущен. Лог: connect() failed (111: Connection refused). Лечение: проверь, что процесс жив и слушает (ss -ltnp, systemctl status).
- Неверный proxy_pass - стучимся не туда (не тот порт/хост/сокет). Сверь upstream: в логе с реальностью.
- php-fpm слушает по unix-сокету, а в конфиге путь/права не те. Лог: connect() to unix:/run/php-fpm.sock failed. Лечение: выровнять listen в pool php-fpm и fastcgi_pass; проверить владельца сокета.
- SELinux на RHEL/Rocky/Alma блокирует исходящее соединение nginx к апстриму. Симптом коварный: процесс жив, порт слушает, а 502 есть. Лог в audit.log. Лечение: setsebool -P httpd_can_network_connect 1 (или httpd_can_network_relay для проксирования). Это реально частая, легко упускаемая причина.
- Бэкенд вернул битый/слишком большой заголовок - не влез в proxy_buffer_size. Лог: upstream sent too big header. Лечение: поднять proxy_buffer_size / proxy_buffers (в эталоне 16k и 16x16k).
Соединение установилось, запрос ушёл, а ответ не пришёл за отведённое время. Лог-маркер: upstream timed out (110: Connection timed out) while reading response header. Лечение:
- Найди, что тормозит: тяжёлый SQL, внешний API, блокировка. Чини на стороне приложения - это первично.
- Если ответ объективно долгий (отчёт, выгрузка) - подними таймауты точечно для этого location, а не глобально: proxy_read_timeout 120s; (по умолчанию и в эталоне - 10s). Глобально задирать опасно: повиснет один зависший бэкенд - и воркеры будут держать соединения дольше.
- Помни про связку с php-fpm: если request_terminate_timeout в php-fpm меньше proxy_read_timeout, 504 даст fastcgi, и таймаут надо поднимать в двух местах.
- proxy_connect_timeout (в эталоне 5s) - это таймаут на сам connect(), а proxy_read/send_timeout - на чтение/отправку. Не путай: connect-таймаут срабатывает, когда сеть не пускает, read-таймаут - когда бэкенд завис уже после соединения.
Чаще nginx решает это сам, не доходя до апстрима. Причины:
- Права файла/каталога: nginx-воркер (от пользователя nginx/www-data) не может прочитать файл. Лог: open() ... failed (13: Permission denied). Лечение: chmod/chown; путь к файлу должен быть исполняемым (x) на всех каталогах выше.
- Запрос каталога при autoindex off и без index-файла. Лог: directory index of "..." is forbidden. Лечение: положить index.html или включить autoindex on (осознанно).
- Явный deny all / deny по IP в конфиге сработал. Проверь блоки allow/deny - в эталоне ими закрыты dotfiles, .git, config-файлы.
- SELinux: контекст файла не httpd_sys_content_t. Лечение: restorecon -Rv /var/www.
Самый частый источник - кривой root/alias/try_files:
- Перепутаны root и alias. Классическая грабля - alias без завершающего слеша (разберём ниже отдельно). Лог: open() "/неожиданный/путь" failed (2). Смотри в лог - там точный путь, который nginx собрал. 90 процентов 404 решаются сравнением "что в логе" против "что на диске".
- try_files $uri $uri/ =404 - последний аргумент вернул 404, потому что ни файл, ни каталог, ни fallback не нашлись.
- Запрос перехватил не тот location (см. порядок location ниже).
nginx режет запрос ДО передачи на бэкенд. Лог: client intended to send too large body. Лечение: client_max_body_size 50m; (по умолчанию 1m). И не забудь поднять upload_max_filesize/post_max_size в php - иначе nginx пропустит, а php отвергнет.
400 / 421 / 426 - уровень протокола.
- 400 Bad Request - битый запрос, слишком длинные/кривые заголовки, мусор в строке запроса. В nginx 1.29.8 появилась директива max_headers для защиты от флуда заголовками - перебор лимита тоже даст 400-семейство.
- 421 Misdirected Request - клиент переиспользовал HTTP/2-соединение для хоста, чей сертификат/SNI не совпадает с этим server-блоком. Лечится корректной раскладкой сертификатов по server_name или отключением переиспользования.
- 426 Upgrade Required - сервер требует апгрейда протокола (например, заворачивает голый HTTP на HTTPS-обязательный эндпоинт).
Эти баги выглядят как порча, но у всех есть скучное объяснение. Они - источник большинства тикетов в nginx troubleshooting.
1. Наследование add_header. Директива add_header не складывается по уровням: если в location есть хоть один add_header, ВСЕ add_header из server/http в этом location исчезают. Поставил в server заголовки безопасности, а в location /api/ добавил один свой - и security-заголовки пропали. Лечение: дублируй нужные заголовки в дочернем блоке либо собирай их в одном месте. Флаг always заставляет слать заголовок даже на 4xx/5xx (в эталоне global_security.conf все заголовки с always - именно поэтому).
2. Порядок и приоритет location. nginx выбирает location НЕ сверху вниз. Сначала проверяются точные (=) и префиксные с ^~, запоминается самый длинный префикс, ПОТОМ regex (~ и ~*) в порядке записи - первый совпавший побеждает. Классика: твой location ~* \.(css|js)$ для статики перехватывает запрос раньше, чем префиксный /api/, и API отдаёт 404. Лечение: критичные префиксы закрывай через ^~ (как location ^~ /api/ в эталоне), чтобы regex до них не дотянулся.
3. alias и завершающий слеш. Самая болезненная 404-грабля:
Код: Выделить всё
# СЛОМАНО: слеши не согласованы
location /static/ {
alias /var/www/assets; # нет слеша на конце
}
# запрос /static/app.js -> nginx ищет /var/www/assetsapp.js -> 404
Код: Выделить всё
# ПРАВИЛЬНО: слеш есть в location И в alias
location /static/ {
alias /var/www/assets/;
}
# запрос /static/app.js -> /var/www/assets/app.js
4. proxy_buffering ломает стриминг. По умолчанию proxy_buffering on - nginx буферизует ответ бэкенда. Для SSE, long-polling, стриминговых API это смерть: клиент не видит данные, пока буфер не заполнится, симптом - "висит и ничего не приходит". Лечение точечно для стрим-локаций:
Код: Выделить всё
location /events {
proxy_pass http://backend;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 1h;
}
6. realip не настроен. Без модуля realip переменная $remote_addr - это адрес ближайшего прокси/CDN, а не клиента. Последствия: в логах один IP на всех, rate-limit по IP не работает (все под одной зоной), geo и auth по IP врут. Лечение - доверять X-Forwarded-For ТОЛЬКО от известных сетей:
Код: Выделить всё
set_real_ip_from 10.0.0.0/8; # только твой балансировщик/CDN
real_ip_header X-Forwarded-For;
real_ip_recursive on;
7. Свежие CVE как причина "странностей". В 2026 держи nginx обновлённым: CVE-2026-42945 (heap overflow в rewrite-модуле, эксплуатируется с мая 2026) - повод минимизировать rewrite-правила; CVE-2026-40460 - спуфинг адреса при миграции HTTP/3-соединения. Если поведение странное и версия старая - сначала обнови, потом дебажь.
Алгоритм диагностики: воспроизводимая последовательность
Когда прилетело "nginx не работает", не импровизируй - иди по шагам:
- 1. Какой код у клиента? curl -i, посмотри статус и заголовки. Это задаёт ветку.
- 2. nginx -t - конфиг вообще валиден? Если нет - чини синтаксис, дальше не иди.
- 3. Открой error_log, найди свежую строку. Прочти syscall + errno + стадию.
- 4. 502/504? -> проверь бэкенд: жив ли (ss -ltnp), доступен ли (curl на upstream напрямую), что в его логах. Сверь upstream: в error_log с реальным адресом.
- 5. 403/404? -> возьми ИЗ ЛОГА точный путь open()... и сравни с диском (ls -la, namei -l). Проверь права, root/alias, try_files.
- 6. Не видно причины? -> debug_connection на свой тестовый IP, error_log debug, воспроизведи запрос, grep по номеру соединения *NNNN.
- 7. Исправил - nginx -t, затем nginx -s reload, повтори запрос, подтверди по логу.
Мини-лаба: воспроизведи 502, 504 и 404 руками
Сделай это на тестовой машине или в контейнере openresty/openresty. Цель - увидеть каждую ошибку в логе своими глазами.
- Шаг 1. Подними nginx с location, проксирующим на заведомо мёртвый порт:
Код: Выделить всё
server {
listen 8081;
error_log /tmp/lab_error.log debug;
location / {
proxy_pass http://127.0.0.1:9999; # тут никто не слушает
proxy_connect_timeout 2s;
}
}
- Шаг 2. nginx -t, затем reload. Сделай curl -i http://127.0.0.1:8081/ - получишь 502. Открой /tmp/lab_error.log, найди connect() failed (111: Connection refused). Это твой эталонный 502.
- Шаг 3. Запусти медленный бэкенд: в одном терминале while true; do printf 'HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\n'; sleep 30; done | nc -l 9999 (или любой скрипт со sleep). Поменяй proxy_pass на порт 9999, добавь proxy_read_timeout 3s; reload.
- Шаг 4. curl -i снова - через 3 секунды получишь 504. В логе ищи upstream timed out (110) while reading response header. Сравни формулировку с 502 из шага 2 - стадия другая (reading, а не connecting).
- Шаг 5. Сделай location / с root /tmp/nowhere; и try_files $uri =404;. Запроси /test.txt - получишь 404. В логе будет open() "/tmp/nowhere/test.txt" failed (2: No such file or directory). Создай файл - и тот же запрос отдаст 200. Ты только что прошёл путь "симптом -> точный путь из лога -> лечение".
- Шаг 6. Сломай alias: location /s/ { alias /tmp/assets; } (без слеша), положи /tmp/assets/x.txt, запроси /s/x.txt. Поймай 404, прочти в логе склеенный путь /tmp/assetsx.txt, добавь слеш в alias, проверь, что починилось.
Контрольные вопросы
- 1. В error_log строка: upstream timed out (110) while reading response header. Какой код увидит клиент и в каком ОДНОМ месте ты сначала будешь искать причину - в nginx или в бэкенде?
- 2. Ты добавил add_header X-Cache в location /api/, и внезапно из этого location пропали заголовки безопасности, заданные в server. Почему и как починить?
- 3. Запрос /static/app.js даёт 404, в логе open() "/var/www/assetsapp.js" failed (2). Где ошибка в конфиге location/alias?
- 4. Ты включил error_log ... debug, а debug-строк в логе нет вообще. Какие две причины проверишь первыми?
Траблшутinга "по наитию" не существует - есть чтение error_log. Запомни главное: код ответа задаёт ветку (502/504 - смотри апстрим, 403/404/413 - смотри сам nginx и файлы), а точная строка лога с syscall, errno и стадией обработки выдаёт причину почти всегда. Включай debug-лог точечно через debug_connection, проверяй итоговый конфиг через nginx -T, держи под рукой компендиум симптом -> причина -> лечение и помни про грабли наследования add_header, порядок location, alias-слеш, buffering и realip. Меняй по одной директиве и сверяйся с логом - тогда любая "nginx ошибка" из паники превращается в пятиминутную процедуру.