В Nginx есть два инструмента, которые часто встречаются рядом в конфигурациях: map и if. Первый выглядит немного непривычно, потому что объявляется не там, где потом используется. Второй окружён легендой nginx if is evil, из-за которой админы иногда либо боятся любого if, либо наоборот игнорируют предупреждение и получают странные 404, пропавшие заголовки, сломанный proxy_pass и циклические редиректы.
Типичная задача: привести все варианты домена к одному canonical host. Например, чтобы www.example.test, example.net и www.example.net навсегда редиректили на example.test, а приложение за reverse proxy видело только нормализованный хост. На бумаге это одна строка rewrite, в продакшене — несколько серверных блоков, SSL-сертификаты на алиасы, правильный server_name, аккуратная работа с location и тесты до перезагрузки.
Короткое правило:
mapиспользуем для вычисления решений,server_name— для выбора виртуального хоста,location— для маршрутизации URI, аifоставляем только для простых и безопасных действий вродеreturn.
Что на самом деле делает map в Nginx
Директива map создаёт переменную на основе другой переменной. Чаще всего берут $host, $uri, $request_method, $http_upgrade, $http_cookie или заголовки клиента. Важно: map объявляется только в контексте http, то есть рядом с server, а не внутри server и не внутри location.
Это частая ошибка: администратор пытается положить map прямо в виртуальный хост, потому что логика нужна именно там. Nginx ответит ошибкой при проверке конфигурации. Правильная модель такая: объявляем карту глобально, а используем созданную переменную в нужном server или location.
Ещё одна полезная особенность: значения map вычисляются лениво. Если переменная, созданная через map, в конкретном запросе нигде не используется, Nginx не тратит время на её вычисление. Поэтому несколько аккуратных карт в большой конфигурации обычно не являются проблемой производительности.
http {
map $host $redirect_host {
default '';
www.example.test example.test;
example.net example.test;
www.example.net example.test;
}
server {
listen 80;
server_name example.test www.example.test example.net www.example.net;
if ($redirect_host) {
return 308 https://$redirect_host$request_uri;
}
return 308 https://example.test$request_uri;
}
}В этом примере $redirect_host будет пустой строкой для всего, что не перечислено в карте. Для известных алиасов она станет example.test. Затем простой if проверяет, есть ли значение, и делает редирект. Такой if не пытается менять внутреннюю маршрутизацию, не создаёт вложенные location-правила и не смешивается с proxy_pass.
Почему говорят nginx if is evil
Фраза nginx if is evil не означает, что директива if всегда запрещена. Проблема в том, где и как её используют. В Nginx if относится к rewrite-модулю, а не к обычной императивной логике как в shell, Python или PHP. Особенно опасны сложные конструкции внутри location, где рядом находятся proxy_pass, fastcgi_pass, add_header, try_files, внутренние редиректы и регулярные выражения.
Безопасным считается сценарий, когда if выполняет немедленное завершение обработки запроса через return. Например, вернуть 301, 308, 403, 404 или 444. Гораздо рискованнее использовать if как универсальный роутер: менять переменные для последующего proxy_pass, вкладывать туда набор rewrite-правил, пытаться включать и выключать заголовки, а потом удивляться, почему соседний location сработал иначе.
Плохой шаблон для canonical host выглядит примерно так:
location / {
if ($host = www.example.test) {
rewrite ^ https://example.test$request_uri permanent;
}
proxy_pass http://app_backend;
proxy_set_header Host $host;
}На простом сайте это может даже работать. Но как только добавятся отдельные location для API, статики, WebSocket, загрузок, внутренних URI или кэша, редирект начнёт жить не в том слое. Canonical host — свойство хоста, а не конкретного пути. Поэтому лучше решать его на уровне server, до выбора сложной маршрутизации приложения.

Canonical host: сначала определитесь с моделью
Перед конфигурацией Nginx полезно письменно зафиксировать, какой хост является каноническим и какие варианты должны на него вести. Это звучит очевидно, но именно здесь часто рождаются петли: HTTP редиректит на HTTPS с www, HTTPS редиректит обратно без www, приложение внутри тоже считает своим основным адресом старый домен, а CDN или балансировщик добавляет собственную нормализацию.
Для большинства сайтов рабочая модель такая:
один canonical host, например
example.test;одна основная схема — обычно HTTPS;
все алиасы домена перечислены в
server_nameтолько там, где они реально обслуживаются или редиректятся;неизвестные хосты попадают в отдельный
default_serverи не доходят до приложения;приложение за reverse proxy получает нормальный
Hostи корректные forwarded-заголовки.
Если у сайта есть несколько легитимных доменов, например публичный сайт, личный кабинет и API, не надо загонять их в один canonical host автоматически. Для каждого сервиса лучше иметь отдельный набор правил и свой server_name. Canonical redirect хорош там, где разные имена действительно являются дублями одной и той же страницы.
Если вы готовите переезд на новый домен, заранее проверьте TTL в DNS, состав сертификатов и цепочки редиректов. Подробный план для такого сценария есть в материале про миграцию домена с 301, HSTS и SSL.
Простой вариант без map: отдельные server-блоки
Если у вас один домен и один алиас www, самый читаемый вариант — отдельный сервер для редиректа. Он проще для сопровождения, понятнее при аудите и почти не оставляет места для двусмысленности.
server {
listen 80;
server_name www.example.test;
return 308 https://example.test$request_uri;
}
server {
listen 80;
server_name example.test;
return 308 https://example.test$request_uri;
}
server {
listen 443 ssl http2;
server_name www.example.test;
ssl_certificate /etc/nginx/ssl/example.test/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/example.test/privkey.pem;
return 308 https://example.test$request_uri;
}
server {
listen 443 ssl http2;
server_name example.test;
ssl_certificate /etc/nginx/ssl/example.test/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/example.test/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3000;
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;
}
}Здесь location приложения вообще не знает про алиасы. Он обслуживает только канонический хост. Это хорошее разделение ответственности: редиректы живут в редирект-серверах, приложение — в основном HTTPS-сервере.
Обратите внимание на HTTPS-алиас. Чтобы Nginx смог принять запрос к www.example.test по TLS и вернуть редирект, сертификат должен покрывать этот хост. Если сертификата для алиаса нет, браузер покажет ошибку сертификата ещё до HTTP-редиректа. Это не баг Nginx, а нормальная логика TLS: сначала устанавливается защищённое соединение, потом передаётся HTTP-запрос. Для публичного проекта удобнее сразу выпускать SSL-сертификаты, покрывающие канонический домен и рабочие алиасы.
Когда nginx map лучше отдельных правил
map становится удобен, когда алиасов много или логика повторяется в нескольких местах. Например, у проекта есть старый домен, домен после ребрендинга, вариант с www, временные маркетинговые домены, но всё должно вести на один canonical host. Можно написать десять server-блоков с одинаковыми return, а можно вынести таблицу соответствий в map.
map $host $canonical_host {
default '';
example.test '';
www.example.test example.test;
old-example.test example.test;
www.old-example.test example.test;
example.net example.test;
www.example.net example.test;
}
server {
listen 80;
server_name example.test www.example.test old-example.test www.old-example.test example.net www.example.net;
if ($canonical_host) {
return 308 https://$canonical_host$request_uri;
}
return 308 https://example.test$request_uri;
}
server {
listen 443 ssl http2;
server_name example.test www.example.test old-example.test www.old-example.test example.net www.example.net;
ssl_certificate /etc/nginx/ssl/example.test/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/example.test/privkey.pem;
if ($canonical_host) {
return 308 https://$canonical_host$request_uri;
}
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-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;
}
}В карте для example.test указана пустая строка. Это значит: хост уже канонический, редирект не нужен. Для алиасов переменная получает значение example.test, и безопасный if на уровне server немедленно завершает запрос через return 308.
Почему 308, а не 301? Оба варианта постоянные. Код 308 сохраняет метод и тело запроса, поэтому аккуратнее для API и форм. Для классического сайта 301 тоже допустим, но важно не менять коды хаотично после индексации. Выберите вариант и используйте последовательно.
$host, $http_host и безопасность редиректов
Для canonical redirect почти всегда лучше использовать $host, а не $http_host. Переменная $http_host содержит заголовок Host почти как пришёл от клиента: может включать порт и нестандартное значение. Переменная $host нормализуется Nginx и не включает порт. Но и $host не стоит бездумно подставлять в редирект как целевой домен.
Главное правило защиты от open redirect: целевой хост должен быть взят из белого списка, а не из пользовательского ввода. Именно поэтому в примерах map возвращает конкретное значение example.test, а не строит адрес из произвольного $host. Если неизвестный домен попал на IP сервера, он не должен превратиться в легитимный редирект куда-то ещё.
server {
listen 80 default_server;
server_name _;
return 444;
}
server {
listen 443 ssl default_server;
server_name _;
ssl_certificate /etc/nginx/ssl/default/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/default/privkey.pem;
return 444;
}Код 444 — особенность Nginx: соединение закрывается без ответа. Это удобно для мусорных хостов, сканеров и случайных запросов по IP. Если такой подход неудобен для диагностики, можно временно возвращать 404 или отдельную техническую страницу, но в продакшене закрытый default_server обычно чище.
Как server_name связан с canonical host
server_name не делает редирект сам по себе. Он помогает Nginx выбрать подходящий виртуальный хост. Если в одном серверном блоке перечислить example.test и www.example.test, это значит только то, что оба имени будут обработаны этим блоком. Дальше уже ваши директивы решают: отдать контент, вернуть редирект, проксировать запрос или закрыть соединение.
Из-за этого встречается ошибка: администратор добавляет алиас в server_name основного сайта и думает, что canonical host настроен. На самом деле сайт начинает отвечать одинаковым контентом на двух хостах. Для пользователя разницы почти нет, а для SEO, кэшей, cookies и аналитики разница есть.
Если хост должен быть алиасом, он должен либо попадать в отдельный redirect server, либо обрабатываться картой map с немедленным return. Если хост должен быть самостоятельным сервисом, ему нужен отдельный server с собственными location и заголовками.
location: где редиректы уместны, а где нет
Директива location выбирает обработчик по URI, а не по домену. Упрощённо порядок такой: сначала проверяются точные совпадения, затем самый длинный префикс, затем регулярные выражения, если префикс не остановил поиск через ^~. Поэтому правила внутри location / не обязательно применятся к /api/, /assets/, /.well-known/ или regex-location для PHP.
Редиректы по пути внутри location нормальны, если задача действительно связана с путём. Например, перенести старый раздел /blog-old/ в /blog/ или убрать устаревший префикс API. Но canonical host лучше не привязывать к location, потому что он должен срабатывать одинаково для любого URI.
location = /old-page {
return 301 /new-page;
}
location ^~ /legacy/ {
return 301 /docs/;
}
location / {
try_files $uri $uri/ @app;
}
location @app {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}Такой location-редирект понятен: он про путь. А вот проверка $host внутри location / — сигнал, что правило, скорее всего, лежит не в том слое.
Reverse proxy: какие заголовки передавать приложению
Когда Nginx работает как reverse proxy, приложение часто строит абсолютные URL, cookies, callback-адреса и редиректы на основе заголовков. Если canonical host настроен в Nginx, но приложение продолжает считать основным адресом www.example.test, возможны циклы: Nginx ведёт на example.test, приложение отвечает редиректом на www.example.test, и браузер ходит по кругу.
Для основного server-блока обычно передают такие заголовки:
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-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;
}Если после canonical redirect в этот блок попадает только example.test, то Host $host корректен. Если же вы сознательно проксируете несколько доменов в одно приложение, проверьте, умеет ли приложение различать их и нет ли собственной настройки основного URL. Для такой схемы обычно удобнее держать Nginx на отдельном VDS, где вы полностью контролируете системные пакеты, сертификаты, upstream и firewall.
Для WebSocket рядом часто используют ещё одну карту. Это хороший пример nginx map, не связанный с canonical host, но полезный в reverse proxy:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
location /socket/ {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
}Здесь map заменяет условную логику: если клиент прислал Upgrade, передаём Connection: upgrade, иначе закрываем как обычное HTTP-соединение. Никакой if в location не нужен.

Как не получить redirect loop
Петли редиректов почти всегда появляются из-за рассинхронизации слоёв. Nginx думает, что он терминирует HTTPS, приложение думает, что запрос пришёл по HTTP. Или внешний балансировщик передаёт X-Forwarded-Proto, Nginx его перезаписывает, а приложение верит не тому заголовку. Или один server редиректит на www, а другой — без www.
Практический чек-лист:
все HTTP-варианты ведут на один HTTPS canonical host;
HTTPS-алиасы имеют сертификат и тоже ведут на canonical host;
основной HTTPS server не делает редирект сам на себя;
в приложении указан тот же основной URL, что и в Nginx;
если перед Nginx есть балансировщик или CDN, схема запроса определяется одинаково на всех слоях;
редиректы по URI не конфликтуют с редиректами по хосту.
При диагностике не смотрите только в браузер. Браузер кэширует постоянные редиректы, HSTS и результаты DNS. Лучше проверять цепочку через curl и временно использовать новый тестовый hostname, если есть подозрение на кэш клиента. Для сложных переездов пригодится и общий чек-лист миграции сайта без простоя.
Проверка конфигурации и безопасная перезагрузка
Перед применением изменений всегда проверяйте синтаксис. Команды одинаковы для большинства Linux-дистрибутивов, где Nginx запущен через systemd.
Debian, Ubuntu, AlmaLinux, Rocky Linux, CentOS Stream, Oracle Linux, Fedora
sudo nginx -t
sudo systemctl reload nginx
sudo systemctl status nginx --no-pagerFreeBSD, если Nginx установлен как сервис
sudo nginx -t
sudo service nginx reload
sudo service nginx statusПроверять редиректы удобно так:
curl -I -H 'Host: www.example.test' http://127.0.0.1/any/path?x=1
curl -I -H 'Host: example.test' http://127.0.0.1/any/path?x=1
curl -k -I --resolve www.example.test:443:127.0.0.1 https://www.example.test/any/path
curl -k -I --resolve example.test:443:127.0.0.1 https://example.test/any/pathВ ответах смотрите на HTTP/1.1 308 или HTTP/2 308, заголовок Location и отсутствие повторного редиректа для уже канонического адреса. Для основного хоста ожидается ответ приложения: 200, 404 на несуществующий путь или другой штатный код, но не бесконечная серия 301/308.
Разбор типовых ошибок
map положили внутрь server
Nginx не примет такую конфигурацию. Перенесите map в контекст http. Чтобы не загромождать основной файл, удобно хранить карты в отдельном файле и подключать его из http-контекста через include.
http {
include /etc/nginx/maps/*.conf;
include /etc/nginx/conf.d/*.conf;
}Редирект сделали через $http_host
Если подставлять $http_host в Location, клиент может влиять на часть адреса. Для canonical redirect используйте белый список в map и возвращайте заранее заданный хост. Это снижает риск open redirect и делает поведение предсказуемым.
HTTPS-алиас не покрыт сертификатом
HTTP-редирект работает, а HTTPS-вариант с www показывает ошибку сертификата. Решение: выпустить сертификат, который включает canonical host и все HTTPS-алиасы, либо не обещать пользователям HTTPS на алиасах. На практике первый вариант почти всегда правильнее.
Canonical host настроен и в Nginx, и в приложении по-разному
Если приложение само делает редиректы, его настройки должны совпадать с Nginx. Особенно внимательно проверьте переменные окружения вроде основного URL, настройки trusted proxy, принудительный HTTPS, cookies domain и генерацию абсолютных ссылок.
if внутри location управляет проксированием
Если внутри location накопилось несколько условий, это повод перепроектировать конфигурацию. Часть логики можно вынести в map, часть — в отдельные location, часть — в отдельные server. Чем раньше Nginx принимает простое решение, тем легче сопровождать конфиг.
Практический шаблон для проекта
Ниже — компактный шаблон, который можно адаптировать для сайта за reverse proxy. Он разделяет мусорные хосты, HTTP-редирект, HTTPS-алиасы и основной backend. В реальном проекте замените домены, пути к сертификатам и адрес приложения.
map $host $canonical_host {
default '';
example.test '';
www.example.test example.test;
old-example.test example.test;
}
server {
listen 80 default_server;
server_name _;
return 444;
}
server {
listen 80;
server_name example.test www.example.test old-example.test;
if ($canonical_host) {
return 308 https://$canonical_host$request_uri;
}
return 308 https://example.test$request_uri;
}
server {
listen 443 ssl http2 default_server;
server_name _;
ssl_certificate /etc/nginx/ssl/default/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/default/privkey.pem;
return 444;
}
server {
listen 443 ssl http2;
server_name example.test www.example.test old-example.test;
ssl_certificate /etc/nginx/ssl/example.test/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/example.test/privkey.pem;
if ($canonical_host) {
return 308 https://$canonical_host$request_uri;
}
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-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;
}
}В этом шаблоне if используется только для немедленного return. Основной location не содержит логики canonical host. Неизвестные имена не проходят к приложению. Карта map остаётся единственным местом, где перечислены алиасы, поэтому при добавлении старого домена не нужно искать rewrite-правила по всей конфигурации.
Итоги
nginx map — хороший способ превратить набор условий в таблицу решений. Для canonical host он особенно полезен, когда алиасов больше одного и хочется держать правила в одном месте. if в Nginx не является абсолютным злом, но его стоит использовать очень узко: на уровне server и с немедленным return. Чем меньше условной логики внутри location, тем предсказуемее работают rewrite-правила, reverse proxy, кэш, WebSocket и приложение.
Самая надёжная архитектура проста: server_name выбирает виртуальный хост, map определяет, нужен ли редирект, return выполняет canonical redirect, а location занимается только URI и проксированием. Такую конфигурацию легко тестировать, безопасно перезагружать и проще поддерживать через год, когда доменов и исключений станет больше.


