Новинка Виртуальный VDS сервер в Нидерландах от 390р
Выберите продукт

Nginx map и if: canonical host, server_name, location и редиректы без петель

Практическая инструкция по Nginx: где использовать map, почему if is evil не всегда означает запрет, как настроить canonical host и не сломать location, SSL и reverse proxy, а также избежать петель редиректов.
Nginx map и if: canonical host, server_name, location и редиректы без петель

В 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, до выбора сложной маршрутизации приложения.

Сравнение роли map и безопасного if в конфигурации Nginx

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-сертификаты, покрывающие канонический домен и рабочие алиасы.

FastFox VDS
Облачный VDS-сервер
Виртуальные серверы с быстрым запуском и гибкой конфигурацией от 390₽ / мес
Доступные локации
Россия Нидерланды

Когда 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 не нужен.

Схема диагностики циклических редиректов в Nginx и приложении

Как не получить 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-pager

FreeBSD, если 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 и проксированием. Такую конфигурацию легко тестировать, безопасно перезагружать и проще поддерживать через год, когда доменов и исключений станет больше.

Поделиться статьей

Вам будет интересно

SSL в Nginx и Apache: цепочка сертификатов, OCSP Stapling и диагностика OpenAI Статья написана AI (GPT 5)

SSL в Nginx и Apache: цепочка сертификатов, OCSP Stapling и диагностика

Разбираем, как правильно собрать цепочку SSL-сертификатов, включить OCSP Stapling в Nginx и Apache, проверить результат через open ...
tuned на Linux VDS: CPU governor, профили performance и powersave OpenAI Статья написана AI (GPT 5)

tuned на Linux VDS: CPU governor, профили performance и powersave

tuned умеет быстро переключать поведение ядра под веб, БД или фоновые задачи, но на VDS часть CPU-настроек зависит от гипервизора. ...
MySQL и MariaDB slow query log на VDS: включение, настройка и анализ pt-query-digest OpenAI Статья написана AI (GPT 5)

MySQL и MariaDB slow query log на VDS: включение, настройка и анализ pt-query-digest

Slow query log помогает найти запросы, которые съедают CPU, диск и время ответа сайта. Показываем настройку MySQL/MariaDB на VDS, ...