В прошлом уроке мы подняли первый рабочий nginx и впихнули весь конфиг в один файл. Пока строк двадцать - это удобно: все на ладони. Но прод так не живет. Полгода спустя в твоем nginx.conf уже восемь сайтов, три апстрима, rate limit, кэш, TLS, и редактировать его страшно: меняешь таймаут для одного location, а ломается совсем другой. Знакомо? Так выглядит nginx конфиг, который вырос без структуры.
Сегодня разберем анатомию по косточкам. Сначала поймем, как nginx вообще читает конфиг - что такое контексты и как директивы наследуются сверху вниз (тут прячется половина всех граблей новичка). Потом возьмем include и распилим монолит на части. И в конце посмотрим, как устроена реальная продакшн-структура nginx config на примере эталонного gateway - чтобы ты не изобретал велосипед, а копировал проверенный паттерн. Плюс два инструмента, без которых отладка превращается в гадание: nginx -t и nginx -T.

Контексты nginx: вложенные коробки
Конфиг nginx - это не плоский список настроек, а дерево вложенных блоков. Каждый блок - это контекст. Самый внешний, который не обернут ни в какие фигурные скобки, называется main (главный, он же глобальный). Внутри него лежат другие контексты:
Код: Выделить всё
# это main-контекст - тут нет внешних скобок
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
events {
# events-контекст: как воркеры обрабатывают соединения
worker_connections 10240;
}
http {
# http-контекст: все про HTTP
include mime.types;
sendfile on;
server {
# server-контекст: один виртуальный хост
listen 80;
server_name example.com;
location / {
# location-контекст: конкретный путь URL
root /var/www/html;
}
}
}
Зачем вообще нужны контексты? Они задают область видимости директивы. listen и server_name имеют смысл только для конкретного виртуального хоста - значит, живут в server. worker_connections - это про модель воркеров, ей место в events, и в server она просто не разрешена, nginx ругнется. Контекст - это ответ на вопрос "к чему применяется эта настройка".
Важно понять модель раз и навсегда: http может содержать много server (много сайтов на одном nginx), server может содержать много location (разные пути одного сайта). Когда приходит запрос, nginx сначала выбирает подходящий server (по listen и server_name из заголовка Host), затем внутри него - подходящий location (по URI). Этот выбор - отдельная большая тема следующих уроков, сегодня нам важнее, как настройки просачиваются вниз по этому дереву.
Простые и блочные директивы
Директивы бывают двух видов. Простые - имя, параметры, точка с запятой в конце:
Код: Выделить всё
worker_processes auto;
gzip on;
proxy_read_timeout 10s;
Код: Выделить всё
location /api/ {
proxy_pass http://backend;
}
Код: Выделить всё
client_max_body_size 16m; # 16 мегабайт
keepalive_time 1h; # 1 час
proxy_read_timeout 10s; # 10 секунд
ssl_session_timeout 10m; # 10 минут, а не мегабайт!
Наследование вниз по контекстам и его грабли
Вот это - сердце урока. Большинство директив, заданных в родительском контексте, наследуются дочерними. Поставил gzip on в http - он действует во всех server и location, если они явно не сказали иначе. Это удобно: общие настройки задаешь один раз наверху.
Но наследование работает по принципу "ближайший выигрывает". Если ту же директиву переопределить ниже - в server или location - локальное значение перебивает унаследованное. Разберем на живом примере, потому что именно тут люди ловят часы отладки:
Код: Выделить всё
http {
gzip on; # включили сжатие глобально
proxy_read_timeout 10s; # таймаут чтения от бэкенда
server {
server_name app.example.com;
# тут gzip и таймаут унаследованы как есть: on и 10s
location / {
proxy_pass http://backend;
# gzip on, proxy_read_timeout 10s - все из http
}
location /report/ {
proxy_pass http://backend;
proxy_read_timeout 120s; # переопределили ТОЛЬКО тут
# тяжелый отчет генерится долго - даем ему 2 минуты
}
}
}
Теперь главная грабля наследования. Возьмем add_header. Многие думают, что заголовки накапливаются - в http добавил один, в location другой, и оба прилетят клиенту. Это не так. add_header (а также expires, и ряд других директив, работающих по принципу "массив директив") наследуется по правилу все или ничего: если в дочернем контексте есть хотя бы один add_header, то ВСЕ add_header из родителя в этом контексте пропадают.
Код: Выделить всё
http {
add_header X-Frame-Options SAMEORIGIN always;
add_header X-Content-Type-Options nosniff always;
server {
location / {
# тут оба заголовка наследуются - все ок
root /var/www;
}
location /api/ {
add_header X-Api-Version 2 always;
# ВНИМАНИЕ: X-Frame-Options и X-Content-Type-Options
# тут НЕ прилетят! Их затер локальный add_header.
proxy_pass http://backend;
}
}
}
Не все директивы наследуются. listen, server_name живут строго в своем server. Но для повседневной работы держи в голове две модели: "обычные директивы - ближайший перебивает" и "add_header-подобные - ближайший обнуляет родителя". Этого хватит, чтобы не попадать в ловушки.
nginx include: режем монолит на части
Монолит мы поняли. Теперь сделаем его поддерживаемым. Директива include - это буквально текстовая вставка: nginx на этапе чтения конфига берет содержимое указанного файла и подставляет его дословно на место include, как будто ты сам туда это вписал. Никакой магии, чистая склейка текста перед парсингом.
Из этого следует важное: include не создает новый контекст. Файл, который ты подключаешь, наследует тот контекст, где стоит сама директива include. Если include написан внутри http - содержимое файла попадает в http-контекст, и внутри файла должны лежать директивы, допустимые в http. Если внутри server - значит, в файле должны быть server-уровневые директивы. Поэтому подключаемые куски нельзя бездумно вставлять куда угодно: место include определяет, что в файле допустимо.
Самый знакомый пример include ты уже видел - mime.types:
Код: Выделить всё
http {
include /etc/nginx/mime.types; # сотни строк type -> расширение
default_type application/octet-stream;
}
include понимает маски (wildcards). Звездочка раскрывается в список файлов по алфавиту:
Код: Выделить всё
http {
# подключить ВСЕ файлы политик по порядку имен
include /etc/nginx/globals/*.conf;
# подключить все включенные сайты
include /etc/nginx/sites-enabled/*.conf;
}
nginx config структура эталонного gateway: модульность по-взрослому
Теперь самое ценное - как организуют nginx config структуру на реальном проде. Возьмем эталонный OpenResty-gateway ngx-trace-gateway. Его nginx.conf - тонкий и почти пустой, вся работа размазана по include. Логика такая: главный файл - это оглавление, а не свалка настроек.
Код: Выделить всё
# nginx.conf - корневой файл, тонкий "диспетчер"
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pcre_jit on; # JIT-компиляция регэкспов, быстрее
include events.conf; # настройки воркеров отдельным файлом
http {
include mime.types;
# карта для апгрейда соединений (WebSocket)
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
charset utf-8;
recursive_error_pages on;
# 1) Глобальные политики - по порядку, каждая в своем файле
include globals/*.conf;
# 2) Сами сайты
include sites-enabled/*.conf;
}
- globals/proxy.conf - все общие proxy_* директивы: таймауты proxy_connect_timeout 5s, буферы, проброс заголовков Host и X-Forwarded-For, политика proxy_next_upstream. Задал раз - наследуется во всех сайтах.
- globals/gzip.conf - gzip on, уровень сжатия, gzip_types. Одна политика сжатия на весь шлюз.
- globals/ssl.conf - ssl_protocols TLSv1.2 TLSv1.3, ssl_session_cache, общие TLS-параметры. Не дублируем в каждом server.
- globals/security.conf - server_tokens off и те самые security-заголовки единым блоком (помнишь грабли add_header? их тут решили на уровне http продуманно).
- globals/rate_limit.conf - зоны limit_req_zone, описанные централизованно.
Зачем так? Три причины. Первая - единая точка правды: меняешь таймаут проксирования в одном файле globals/proxy.conf, и он применяется ко всем сайтам сразу, нигде не забыл. Вторая - читаемость: открыл sites-enabled/shop.conf и видишь логику именно этого сайта, не утопая в общих настройках. Третья - безопасное добавление/выключение сайтов: чтобы погасить сайт, достаточно убрать симлинк из sites-enabled (паттерн sites-available + sites-enabled через симлинки - классика). Это и есть зрелая настройка nginx конфигурации: монолит распилен на политики и хосты.
Подсмотри в память по @realistic-content-seeding мысль о структуре - тут она тоже работает: лучше один проверенный модульный каркас, чем десяток одноразовых правок в монолите.
nginx -t и nginx -T: проверка и рентген конфига
Распилив конфиг на include, ты получаешь новую проблему: настройки разбросаны по двадцати файлам, и понять итоговую картину глазами тяжело. Тут спасают две команды.
nginx -t - проверка синтаксиса без применения. Парсит весь конфиг (со всеми include) и говорит, валиден ли он. Запускай ВСЕГДА перед reload - иначе рискуешь уронить работающий nginx кривым конфигом:
Код: Выделить всё
$ nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
Код: Выделить всё
$ nginx -t
nginx: [emerg] unknown directive "proxy_reed_timeout" in /etc/nginx/globals/proxy.conf:4
nginx: configuration file /etc/nginx/nginx.conf test failed
nginx -T (большая T) - то же тестирование плюс дамп всего итогового конфига в стандартный вывод, со всеми раскрытыми include. Это рентген: ты видишь финальную склеенную конфигурацию ровно так, как ее видит сам nginx, с пометками, из какого файла приехал каждый кусок. Бесценная вещь для отладки модульной структуры:
Код: Выделить всё
$ nginx -T | less
# configuration file /etc/nginx/nginx.conf:
worker_processes auto;
...
# configuration file /etc/nginx/globals/proxy.conf:
proxy_connect_timeout 5s;
proxy_read_timeout 10s;
...
# configuration file /etc/nginx/sites-enabled/shop.conf:
server {
listen 80;
...
Код: Выделить всё
nginx -T | grep -n proxy_read_timeoutПосле того как nginx -t прошел, применяем без даунтайма:
Код: Выделить всё
$ nginx -s reload # или: systemctl reload nginx
Как читать чужой nginx конфиг
Тебе постоянно будут падать незнакомые конфиги - на новой работе, в чужом проекте, в гайде из интернета. Алгоритм чтения:
- Начни с корневого nginx.conf и иди по include как по оглавлению - построй в голове дерево файлов.
- Не доверяй глазам - сделай nginx -T и читай итоговую склейку. Только она показывает реальность с учетом порядка include и переопределений.
- Для конкретной директивы делай grep по дампу: где задана, в каком контексте, не перебита ли ниже.
- Помни про наследование: настройка в location могла прийти из http через три уровня, ее там физически не видно.
- Отдельно проверь add_header - не затерты ли security-заголовки локальным блоком.
Мини-лаба: распили монолит сам
Повтори руками, это 15 минут и навсегда закрепит модель. Работаем на тестовом nginx (можно в Docker-образе из прошлого урока).
- Шаг 1. Возьми монолитный nginx.conf из прошлого урока (один server с парой location). Проверь: . Должно быть ok.
Код: Выделить всё
nginx -t - Шаг 2. Создай каталог /etc/nginx/globals/. Вынеси из http в файл globals/gzip.conf три строки: gzip on; gzip_comp_level 5; gzip_min_length 1024;. В nginx.conf на их место впиши
Код: Выделить всё
include globals/*.conf; - Шаг 3. Проверь - должно остаться ok. Затем
Код: Выделить всё
nginx -t- убедись, что директивы на месте и приехали из globals/gzip.conf.Код: Выделить всё
nginx -T | grep gzip - Шаг 4. Теперь спровоцируй грабли наследования. В http добавь , а в один из location добавь
Код: Выделить всё
add_header X-Test base always;. Сделай reload и проверь заголовки:Код: Выделить всё
add_header X-Local local always;. Убедись, что X-Test пропал на этом пути - вот оно, обнуление add_header. На другом location X-Test есть.Код: Выделить всё
curl -I http://localhost/тот-location - Шаг 5. Сломай конфиг нарочно: убери точку с запятой в globals/gzip.conf. Запусти и прочитай, как точно nginx указал файл и строку. Верни точку с запятой.
Код: Выделить всё
nginx -t - Шаг 6. Финал: . Связка через && гарантирует, что reload не выполнится при битом конфиге.
Код: Выделить всё
nginx -t && nginx -s reload
- Ты задал proxy_read_timeout 10s в http и proxy_read_timeout 60s в одном location. Какое значение действует в этом location, а какое - в соседнем location, где директива не указана?
- В http стоят два add_header с security-заголовками. В location /api/ ты добавил один свой add_header. Что прилетит клиенту на /api/ и почему это опасно?
- Чем nginx -T отличается от nginx -t и в какой ситуации именно -T спасет тебя за минуту вместо часа отладки?
- include подключает файл внутри http-контекста. Можно ли в этом файле написать директиву listen 80; и почему?
Конфиг nginx - это дерево контекстов main -> events -> http -> server -> location (плюс stream для L4). Директивы наследуются вниз, и ближайший контекст перебивает родительский - а add_header-подобные и вовсе обнуляют родительские при любом локальном вхождении, это главная ловушка. include - это дословная текстовая вставка, не создающая контекста; через wildcard-маски в алфавитном порядке он позволяет распилить монолит на политики (globals/*) и сайты (sites-enabled/*), как в эталонном gateway: тонкий nginx.conf-оглавление, тяжелая обвязка в глобальных политиках, короткие виртуальные хосты сверху. И два инструмента на каждый день: nginx -t проверяет синтаксис перед каждым reload, nginx -T дампит итоговую склейку для отладки include и наследования. В следующем уроке возьмемся за то, как nginx выбирает server и location для входящего запроса - механику маршрутизации.