Ты добавил блок для статики, перезагрузил nginx - и внезапно /api/v1/users отдаёт 404, хотя бэкенд жив. Или наоборот: ты закрыл /admin авторизацией, а злоумышленник зашёл через /admin.php в обход. В 9 случаях из 10 виновата не магия, а то, как nginx выбирает location. Это самая частая засада новичков и одновременно тема, на которой спотыкаются даже сеньоры, когда конфиг разрастается до сотни блоков.
Главная мысль урока: nginx не читает location-блоки сверху вниз и не берёт "первый подходящий". Порядок в файле для префиксов вообще не важен, а для regex - важен, но иначе, чем ты думаешь. Есть строгий алгоритм из четырёх шагов, и если ты держишь его в голове, nginx location перестаёт быть лотереей. Разберём механику до винтика, прогоним руками четыре URL и научимся ловить классическую ошибку, когда regex перехватывает запрос раньше нужного префикса.
Чтобы дальше всё было однозначно: location живёт внутри блока server (а иногда вложен в другой location), и матчинг идёт по нормализованному URI - без query string, после декодирования %XX и схлопывания /../ и //. То есть на /api/%2e%2e/secret?x=1 location видит уже /secret, а ?x=1 отброшен. Это важно: фильтровать query string внутри location бессмысленно, для этого есть $args и map. В эталонном ngx-trace-gateway ровно поэтому проверки SQLi/XSS по $query_string вынесены отдельно, а не навешаны на location.

Пять с половиной типов location и что значит каждый модификатор
Запомни модификаторы - это половина успеха. Их ровно столько:
- Префиксный (без модификатора): location /images/ - совпадает, если URI начинается с /images/. Это самый обычный тип. Таких блоков может совпасть несколько (например /, /images/, /images/avatars/), и nginx запомнит самый длинный.
- = (точное совпадение): location = / - совпадает, только если URI равен строке буква-в-букву. Никаких "начинается с". Самый быстрый матч и самый высокий приоритет.
- ^~ (префикс без regex): location ^~ /img/ - тоже префиксный, но с особым свойством: если этот блок оказался самым длинным префиксом, nginx не будет перебирать regex вообще. Это "стоп-сигнал".
- ~ (regex, чувствительный к регистру): location ~ \.php$ - совпадает по регулярному выражению PCRE, A и a - разные буквы.
- ~* (regex, нечувствительный к регистру): location ~* \.(jpg|png|css)$ - то же, но .JPG и .jpg равнозначны.
- @ (именованный): location @fallback - вообще не участвует в обычном матчинге URL. В него нельзя попасть запросом снаружи, в него прыгают изнутри через try_files ... @fallback или error_page 500 @fallback.
Точный алгоритм выбора location: четыре шага, которые решают всё
Вот сердце урока. Nginx на каждый запрос выполняет ровно эту последовательность. Заучи её как таблицу умножения - это и есть nginx location order.
- Шаг 1. Ищется точное совпадение по = . Если нашлось - немедленно стоп, этот location выигрывает, дальше ничего не смотрится. Поэтому location = / самый дешёвый: для корня сайта nginx не перебирает вообще ничего.
- Шаг 2. Перебираются все префиксные блоки (обычные и ^~) и запоминается тот, чей префикс самый длинный (по числу совпавших символов). Порядок в файле тут не важен - nginx сравнивает длины. Просто запомнили кандидата, пока ничего не решили.
- Шаг 3. Смотрим на запомненный префикс. Если он с модификатором ^~ - стоп, берём его, regex не трогаем. Если он обычный префиксный - идём дальше.
- Шаг 4. Перебираются regex-блоки (~ и ~*) строго в порядке их записи в конфиге, сверху вниз. Первый совпавший выигрывает - и остальные regex уже не проверяются. Если ни один regex не совпал - возвращаемся к запомненному на шаге 2 префиксу.
Код: Выделить всё
= (точное) <- абсолютный победитель, мгновенный стоп
^~ (самый длинный) <- стоп, regex не смотрятся
~ / ~* (regex) <- первый по порядку в файле
префикс (самый длинный) <- запасной вариант, если regex не сработал
nginx location примеры: пошаговый прогон четырёх URL
Теория мертва без прогона. Возьмём конкретный server и руками проследим, куда уйдёт каждый из четырёх запросов. Это тот самый навык, который отличает того, кто "вроде понял", от того, кто умеет читать конфиг.
Код: Выделить всё
server {
listen 80;
server_name shop.local;
root /var/www/shop;
location = / {
return 200 "EXACT root\n";
}
location / {
return 200 "PREFIX slash (fallback)\n";
}
location /api/ {
return 200 "PREFIX api\n";
}
location ^~ /img/ {
return 200 "PREFIX-stop img\n";
}
location ~* \.(png|jpg|css)$ {
return 200 "REGEX static\n";
}
location ~ ^/api/v1 {
return 200 "REGEX api v1\n";
}
}
URL 1: /
Шаг 1 - есть ли точное = / ? Да, location = / . Стоп. Победитель: EXACT root. Заметь: location / (обычный префикс) тоже подходит, но = всегда бьёт его и обрывает поиск мгновенно.
URL 2: /img/a.png
Шаг 1 - точного = /img/a.png нет. Шаг 2 - какие префиксы подходят? location / (длина 1) и location ^~ /img/ (длина 5). Самый длинный - /img/ . Шаг 3 - у запомненного префикса модификатор ^~ ? Да! Стоп, regex не смотрим вообще. Победитель: PREFIX-stop img. Обрати внимание: regex ~* \.png$ тоже подошёл бы, но ^~ не дал ему шанса. Это и есть смысл ^~ - защитить каталог от перехвата regex-ом.
URL 3: /api/v1/orders
Шаг 1 - точного нет. Шаг 2 - подходят / (длина 1) и /api/ (длина 5). Запоминаем /api/ как самый длинный, он обычный префикс (без ^~). Шаг 3 - он не ^~, идём дальше. Шаг 4 - перебираем regex по порядку: сначала ~* \.(png|jpg|css)$ - URI не оканчивается на эти расширения, мимо; затем ~ ^/api/v1 - совпадает! Стоп. Победитель: REGEX api v1. Вот он, классический случай: твой добротный префикс /api/ проиграл regex-у. Если бы ты этого не хотел, написал бы location ^~ /api/ .
URL 4: /style.css
Шаг 1 - точного нет. Шаг 2 - подходит только location / (длина 1), запоминаем. Шаг 3 - не ^~. Шаг 4 - regex по порядку: ~* \.(png|jpg|css)$ - оканчивается на .css, совпало! Стоп. Победитель: REGEX static. А location / остался лишь запасным вариантом, который не понадобился.
Проверить можно руками, не гадая:
Код: Выделить всё
$ curl -s http://shop.local/
EXACT root
$ curl -s http://shop.local/img/a.png
PREFIX-stop img
$ curl -s http://shop.local/api/v1/orders
REGEX api v1
$ curl -s http://shop.local/style.css
REGEX static
nginx location regex и вложенные блоки: тонкости эксплуатации
Несколько вещей, которые в брифе теории не видны, но бьют в проде.
Regex проверяются в порядке файла, и это единственный тип, где порядок решает. Если у тебя два пересекающихся regex - например ~* \.(jpg|jpeg)$ и ~* \.jpeg$ - то второй никогда не сработает, его съест первый. Поэтому более специфичные regex ставь выше общих. Для префиксов же порядок не значит ничего: можешь располагать их как удобно читать, nginx всё равно возьмёт самый длинный.
Вложенные location. location можно положить внутрь location. Это удобно: внешний ловит каталог, внутренний - частный случай. Алгоритм матчинга при этом запускается заново уже внутри выбранного родителя - и только по его потомкам.
Код: Выделить всё
location /download/ {
internal;
location ~* \.(zip|tar\.gz)$ {
add_header Content-Disposition "attachment";
}
}
Слеш и нормализация. location /api (без слеша) совпадёт и с /api, и с /apiary, и с /api/v1 - это просто префикс по символам, ему всё равно на границы пути. Если нужен именно каталог - пиши location /api/ со слешем. В эталонном проекте этот нюанс решён через map $uri в $needs_slash и канонизацию редиректами, чтобы /api и /api/ не плодили дублей для SEO и кэша.
Стоимость. Префиксный матч - это поиск по дереву, практически бесплатный даже на сотнях location. Regex - линейный перебор: каждый запрос гоняет PCRE по списку выражений сверху вниз, пока не совпадёт. На горячем пути (статика, health-check) ставь = или ^~, чтобы вообще не доходить до regex. И помни про CVE-2026-42945 в rewrite-модуле: чем меньше regex и rewrite в конфиге, тем меньше поверхность атаки и тем проще аудит.
Типичные грабли nginx location
- Regex перехватил раньше нужного префикса. Самая частая. location /api/v1/users/ написан, а запросы на .json/.xml внутри него уводит общий regex статики. Лечение: либо ^~ на нужном префиксе, либо более узкий regex, либо вынести статику в отдельный каталог.
- Думают, что location читается сверху вниз. Для префиксов - нет, берётся самый длинный независимо от позиции. Перестановка двух префиксных блоков ничего не меняет; перестановка двух regex - меняет всё.
- Путают = и префикс. location = /health ловит ровно /health и ничего больше; location /health поймает ещё и /health-check, /healthz. Для эндпоинтов проверки всегда =, иначе случайно накроешь лишнее.
- Обход через регистр и хвост. location ~ \.php$ (чувствительный к регистру) не поймает /shell.PHP, а апач за бэкендом - может выполнить. Для запрещающих правил по расширению бери ~* . Аналогично location = /admin не закроет /admin/ - это уже другой URI.
- Забывают, что @-локейшен снаружи недоступен и наоборот. Нельзя сделать proxy_pass на /@backend запросом; @ - только цель для try_files и error_page.
Десять минут, и алгоритм осядет в пальцах.
- 1. Подними тестовый nginx (или контейнер openresty/openresty:latest) и создай server из примера выше, где каждый location делает return 200 со своим именем-ярлыком.
- 2. Проверь синтаксис: и перезагрузи:
Код: Выделить всё
nginx -tКод: Выделить всё
nginx -s reload - 3. Прогони все четыре curl из урока и сверь вывод с прогноном. Совпало - ты читаешь алгоритм правильно.
- 4. Теперь сломай специально: поменяй location ~ ^/api/v1 на location ^~ /api/v1/ и снова дёрни /api/v1/orders. Должно стать PREFIX, а не REGEX - ты на шаге 3 включил стоп-сигнал.
- 5. Поменяй местами два regex-блока и запроси /style.css . Убедись, что для regex порядок в файле решает (а для префиксов - проверь, что перестановка /api/ и /img/ ничего не меняет).
- 6. Добавь location = /health { return 200 "ok"; } и запроси /health и /healthz - почувствуй разницу между точным и префиксным.
- 1. Есть location / , location /static/ и location ~* \.css$ . Куда уйдёт запрос /static/app.css и почему именно туда?
- 2. Чем поведение location ^~ /img/ отличается от location /img/ при наличии regex-блока на картинки? На каком шаге алгоритма разница?
- 3. Два regex: ~ \.(gif|png)$ выше и ~ \.png$ ниже. Какой поймает /logo.png и почему второй бесполезен?
- 4. Почему для эндпоинта /health правильнее location = /health , а не location /health ?
Location выбирается не по порядку в файле, а по алгоритму: точное = бьёт всё и обрывает поиск; иначе запоминается самый длинный префикс; ^~ на нём = стоп без regex; иначе перебираются regex сверху вниз и побеждает первый; и только если regex молчат - срабатывает запомненный префикс. Держи в голове таблицу = > ^~ > regex(по порядку) > префикс, помни про ловушку "regex перехватил префикс", закрывай горячие пути через = и ^~ - и nginx location перестанет тебя удивлять. В следующих уроках на этот скелет ляжет proxy_pass, try_files и кэш - но маршрут запроса всегда начинается здесь.