✻ Урок 2.5 · Тема 2: Сеть, HTTP, DNS, nginx и TLS
nginx как reverse proxy для «Заметок»
Содержание урока
Зачем это нужно
«Заметки» слушают 127.0.0.1:8080 и напрямую в интернет не выставляются. Приложение написано на простом Python-сервере: он обслуживает клиентов без защиты и очередей, не умеет шифровать трафик (то есть данные, которые ходят по сети: без шифрования их может прочитать любой, через чьи сети они проходят; шифрование называется TLS, подробно в уроке 2.6), не умеет ограничивать нагрузку (например, не пускать одного клиента, который шлёт запросы тысячами в секунду) и отдавать файлы быстро. Очередь здесь значит вот что: когда клиентов больше, чем сервер успевает обслужить, лишние встают в ожидание по порядку, как люди у кассы; без аккуратной очереди они получают ошибки. Поэтому перед приложением ставят отдельную программу, которая принимает всех посетителей на стандартном порту 80 (порт это номер «двери» на машине, урок 2.2; 80 это дверь, в которую браузер стучится по умолчанию) и передаёт запросы приложению. Такая программа называется обратным прокси (reverse proxy), а самая распространённая из них это nginx (читается «энджин-икс»).
На работе ты будешь видеть nginx почти везде: перед сайтами, API (адресами, по которым одни программы обращаются к другим), Grafana (сайт с графиками о работе систем, урок 8.6) и кластерами Kubernetes (системой, которая запускает и перезапускает приложения на многих машинах, урок 5.1). Самая частая жалоба «сайт лежит» на деле выглядит как ошибка 502 Bad Gateway («плохой шлюз»: nginx работает, а приложение за ним не ответило) или 504 Gateway Timeout («шлюз не дождался»: приложение отвечает слишком долго; подробно о таймаутах в уроке 2.8). Разбор начинается с чтения error.log (журнала ошибок nginx: текстовый файл, куда он пишет, что пошло не так), а не с перезапуска всего подряд. Этот навык ты получишь здесь.
Шаг проекта: «Заметки» открываются на http://notes.lab/ через nginx на порту 80, а приложение получает настоящий адрес и схему клиента в заголовках X-Forwarded-*. Конфиг (файл с настройками) nginx хранится в репозитории как deploy/nginx/notes.conf. Заголовки X-Forwarded-* это дополнительные строки в запросе, где nginx сообщает приложению, откуда на самом деле пришёл клиент (разберём ниже). Репозиторий (~/notes: папка проекта, за изменениями в которой следит Git, тема 3) хранит и конфиг. Код app.py не меняется, остаётся v3 из урока 2.4.
Что нужно знать
- Урок 1.2: текст и пайпы:
grep,awk,sort,uniq -c,|. С их помощью читаемaccess.log(журнал доступа nginx: по строке на каждый запрос, кто, что и когда просил). - Урок 1.3: пользователи и права:
sudo, владелец файла, почему порты меньше 1024 открывает только root. - Урок 1.4: процессы и сигналы: что такое процесс и PID, зачем нужны сигналы.
- Урок 1.8: systemd:
systemctl,journalctl, сервисnotes.service, файл/etc/notes/notes.env. - Урок 2.1: адреса и маршруты:
127.0.0.1(loopback) и0.0.0.0, адрес привязкиHOST(на каком адресе программа слушает). - Урок 2.2: порты и TCP: порт,
Connection refused,ss -ltn. - Урок 2.3: DNS: запись
127.0.0.1 notes.labв/etc/hosts. - Урок 2.4: HTTP: запрос и ответ, заголовки, коды,
curl -i, эндпоинты/headers,/slowи/errorизapp.pyv3.
Если у тебя app.py старше v3, скачай эталон: versions/v3.py.
Картина целиком
Перед гостиницей два слова, которые встретятся в схеме. Блок (block) в конфиге nginx это кусок настроек в фигурных скобках { ... }. Блок server описывает один сайт, блок location внутри него описывает, что делать с запросами по конкретному пути, а команда proxy_pass внутри location значит «передай запрос вот этому приложению». Подробно разберём ниже.
Представь гостиницу. Гости не ходят по номерам сами: они подходят к администратору на ресепшене. Администратор выслушивает просьбу, звонит в нужный номер, передаёт ответ гостю. Гость даже не знает номер комнаты, с которой его соединили. Так администратор может закрыть дверь на кухню, сменить комнату, в которой идёт ремонт, и следить за очередью у стойки, а комнаты остаются тихими.
nginx это администратор, а «Заметки» это номер, в который он звонит. Гость (клиент, например curl или браузер) приходит на адрес notes.lab, порт 80. nginx открывает вторую линию до 127.0.0.1:8080 и пересказывает запрос. Аналогия перестаёт работать в одном: администратор не может подделать паспорт гостя, а nginx может (и должен) сам написать приложению, с какого адреса пришёл клиент, потому что для приложения все запросы приходят от nginx.
flowchart LR
C["Клиент<br>curl или браузер"] -->|"соединение 1"| N["nginx (порт 80)<br>1. выбрал server<br>2. выбрал location<br>3. дописал заголовки<br>4. пишет логи"]
N -->|"соединение 2"| A["«Заметки» (порт 8080)<br>app.py слушает<br>только 127.0.0.1"]
N -.-> L1["notes-access.log<br>кто и что просил"]
N -.-> L2["notes-error.log<br>что пошло не так"]
Видно два отдельных соединения: клиент говорит только с nginx, а приложение только с nginx. Логи nginx пишет сам.
В уроке разбираются по порядку все стрелки и блоки этой схемы:
- что такое обратный прокси и почему соединений два;
- как устроены процессы nginx и как менять его настройки, не роняя сайт;
- как выглядит конфиг и где его файлы;
- как nginx выбирает блок
serverи блокlocationдля запроса; - как работает
proxy_passи зачем заголовкиX-Forwarded-*; - откуда берутся коды 502 и 504 и как читать логи.
Теория
Обратный прокси: зачем нужен посредник и почему соединений два
Программа-приложение решает одну задачу: показывать заметки. Всё вокруг неё (принять тысячи подключений, зашифровать трафик, выдержать медленных клиентов, ответить ошибкой, когда приложение упало) она делает плохо или вообще не умеет. Простой Python-сервер из app.py держит каждое подключение отдельным потоком (поток это отдельная «рука» программы: одна рука обслуживает одного клиента, и число рук ограничено), и если тысяча клиентов по слабой мобильной сети начнут медленно читать ответ, потоки закончатся. Без посредника разработчик обязан встроить всё это в каждое приложение, а при каждой замене приложения менять и то, что видят клиенты.
Слово «прокси». Прокси (proxy, «посредник») это программа, которая принимает запрос от имени кого-то другого и передаёт его дальше. Бывают два вида:
- прямой прокси стоит на стороне клиента: ты за ним прячешься, чтобы сайт не видел твой адрес (например, корпоративный прокси, через который сотрудники выходят в интернет);
- обратный (reverse) стоит на стороне сервера: клиенты обращаются к нему, а он выбирает, какому приложению передать запрос. Клиент даже не знает, что приложение там есть. Наш случай.
Вспомни администратора гостиницы из начала урока. Клиент говорит с администратором и получает ответ от него, а не от повара на кухне.
Теперь по шагам. Слово upstream (буквально «выше по течению») в nginx означает «то приложение, куда передаём запросы». У нас это «Заметки» на 127.0.0.1:8080. Когда клиент открывает http://notes.lab/:
- Клиент по DNS (урок 2.3) узнаёт адрес
notes.labи открывает TCP-соединение (урок 2.2) на порт 80. Порт 80 это стандартный порт HTTP: клиент подставляет его сам, если в адресе порта нет. - Клиент отправляет HTTP-запрос:
GET / HTTP/1.1, заголовокHost: notes.lab. - nginx читает запрос целиком, решает, что с ним делать (как, разберём ниже), и открывает второе TCP-соединение, уже к
127.0.0.1:8080. - nginx отправляет приложению запрос (свой, заново собранный).
- Приложение отвечает nginx. nginx передаёт ответ клиенту по первому соединению.
Итог: два независимых соединения. Клиент и приложение друг друга не видят.
У меня на стенде «Заметки» отвечали напрямую на /headers, эндпоинт которого возвращает заголовки, которые приложение получило (он появился в уроке 2.4):
$ curl -s http://127.0.0.1:8080/headers
{"Host": "127.0.0.1:8080", "User-Agent": "curl/8.5.0", "Accept": "*/*"}
Приложение видит Host: 127.0.0.1:8080, потому что клиент так написал. А вот тот же запрос через nginx (после настройки в практике):
$ curl -s http://notes.lab/headers
{"Host": "notes.lab", "X-Real-IP": "127.0.0.1", "X-Forwarded-For": "127.0.0.1", "X-Forwarded-Proto": "http", "Connection": "close", "User-Agent": "curl/8.5.0", "Accept": "*/*"}
Появились четыре новых заголовка от nginx и Connection: close. Про каждый расскажем в разделе о заголовках. Доказательство, что соединений два, видно в заголовках: клиенту nginx ответил Connection: keep-alive («соединение оставляю открытым для следующих запросов»), а приложению написал Connection: close («после ответа закрывай»), и в журнале приложения запрос записан как HTTP/1.0. У приложения своя жизнь: разные версии протокола, разные настройки.
Осторожно: обратный прокси не редирект (redirect). При редиректе сервер говорит клиенту «иди по другому адресу», и клиент сам идёт туда, видя новый адрес в строке. При проксировании клиент вообще не узнаёт, что за nginx есть другой порт. И ещё: приложение не «за файрволом», просто оно доступно только через 127.0.0.1. Разница важна, об этом мы поговорим в конце теории.
Прикинь сам: узнает ли приложение, что клиент пришёл с адреса
203.0.113.5, если запрос прошёл через nginx?
Само нет: соединение к нему открывает nginx, и адрес клиента приложение не видит. Помогут заголовки X-Forwarded-For, о них ниже.
Главное: обратный прокси принимает запрос от клиента и открывает второе, отдельное соединение к приложению.
Проверь понимание: сколько TCP-соединений открыто, пока клиент ждёт ответ от приложения через nginx? Кто с кем соединён?
Ответ
Два. Первое: клиент и nginx (порт 80). Второе: nginx и приложение (127.0.0.1:8080). Приложение не знает про первое соединение, а клиент не знает про второе.
Один nginx, два соединения и живой сайт: теперь нужно понять, как сам nginx устроен изнутри и как менять его настройки, не уронив запросы.
Процессы nginx: master, worker, reload и nginx -t
Конфиг на сервере правят «вживую», пока сайт обслуживает людей. Нужно понимать, как применить новые настройки, не выкинув тех, кто прямо сейчас качает страницу, и как не уронить сайт опечаткой.
Что запускается. После установки nginx работает как несколько процессов (процесс это запущенная программа, урок 1.4):
- один master (главный): читает конфиг, открывает порт 80, управляет остальными. Сам запросы не обслуживает. Работает от
root, потому что порты меньше 1024 разрешено открывать только суперпользователю (урок 1.3); - несколько worker (рабочие): принимают соединения и обслуживают запросы. Работают от непривилегированного пользователя
www-data. Если в worker найдут уязвимость (ошибку в программе, которой может воспользоваться злоумышленник), у взломщика окажутся права обычного пользователя, а не root.
Число worker задаёт строка worker_processes auto; в главном конфиге: auto значит «по числу ядер процессора».
Представь администратора гостиницы и бригаду сменщиков. Старший смены (master) не обслуживает гостей, он расставляет людей и меняет бригаду, а гостей встречают сами сменщики (worker).
Как применяются новые настройки. Есть два способа, и различие принципиальное:
- reload (перечитать). Master получает сигнал (урок 1.4), читает новый конфиг, проверяет его и, если он верен, запускает новых worker с новым конфигом. Старым worker он велит: «доработайте текущие запросы и завершайтесь». Никого не выбросило: кто уже был подключён, дорабатывает у старого worker, новые клиенты идут к новым. Если конфиг сломан, старые worker продолжают работать, сайт живёт на прошлом конфиге.
- restart (перезапуск). Все процессы останавливаются и запускаются заново. Соединения рвутся, на мгновение порт 80 закрыт.
Команда для reload: sudo systemctl reload nginx (через systemd, урок 1.8). Перед ней принято запускать проверку синтаксиса: sudo nginx -t (test). Она читает все файлы конфига, ничего не применяет и печатает, всё ли в порядке или на какой строке ошибка.
Так выглядит список процессов на моём стенде (15 ядер, поэтому 15 worker; у тебя число другое):
PID USER CMD
431 root nginx: master process /usr/sbin/nginx -g daemon on; master_process on;
432 www-data nginx: worker process
433 www-data nginx: worker process
...
После sudo systemctl reload nginx номер master остался прежним (431), а у worker номера сменились (было 432, стало 1594, потом 1618): это как раз новая бригада. Строка с master показывает -g daemon on; master_process on;: это флаги запуска, которые добавил systemd, значения по умолчанию, их можно не трогать.
Осторожно: правку файла nginx не подхватывает сам. Изменил конфиг и не выполнил reload, значит, работает старая версия. Сам reload асинхронный: команда возвращается сразу, а новые worker поднимаются за доли секунды, поэтому запрос сразу после него можно получить по старому конфигу. В скриптах я делаю паузу в секунду, а если результат «не тот», повторяю запрос. И reload не спасает от ошибки в конфиге: он просто откажется применять. Проверка nginx -t это бесплатная страховка, которая говорит о проблеме до того, как ты нажал Enter.
Прикинь сам: ты правишь конфиг и сразу делаешь
reload, а в файле опечатка. Что станет с сайтом?
Ничего: nginx проверит конфиг, откажется его применять и продолжит работать по старому. Поэтому перед reload запускают nginx -t.
Главное:
reloadменяет worker без обрыва запросов, аnginx -tпроверяет конфиг заранее.
Проверь понимание: почему после
nginx -s reload(то же, что systemctl reload) долгий запрос, который уже идёт, не обрывается?
Ответ
Master запускает новых worker с новым конфигом, а старым говорит завершиться, но только после того, как они закончат текущие запросы. Пока старый worker не завершил свой запрос, он жив, и соединение остаётся открытым. При restart все процессы убиваются сразу.
Перезагружать мы умеем, но что именно писать в конфиге и как он разложен по файлам, пока непонятно. К этому и переходим.
Конфиг nginx: директивы, блоки и файлы
Один nginx обслуживает десятки сайтов, у каждого свои правила. Чтобы описать их все в одном месте и не запутаться, конфиг делят на вложенные блоки: общее на верхнем уровне, частное внутри.
Из чего состоит конфиг. Конфигурационный файл (файл настроек программы, урок 1.8) nginx состоит из двух видов конструкций:
- директива (directive): одна настройка, «имя значение;». Заканчивается точкой с запятой. Например,
listen 80;значит «принимай на порту 80». Забытая;это самая частая опечатка; - блок (block, он же контекст, context): группа директив в фигурных скобках, у которой есть имя. Например,
server { ... }. Блоки вкладываются друг в друга.
Директиву можно писать только в «своём» блоке. Например, proxy_pass разрешён внутри location, а listen только внутри server. Ошибка directive is not allowed here означает «настройка стоит не там».
Схема вложенности:
nginx.conf
├─ user www-data; <- главный уровень: настройки процессов
├─ worker_processes auto;
├─ events { ... } <- как принимать соединения
└─ http { <- всё про HTTP
├─ access_log ...; <- общий лог
├─ include /etc/nginx/sites-enabled/*; <- подключить чужие файлы
└─ server { <- один «сайт» (виртуальный сервер)
├─ listen 80;
├─ server_name notes.lab;
└─ location / { <- правило для группы путей
└─ proxy_pass http://127.0.0.1:8080;
}
}
}
Директива include подставляет на своё место содержимое других файлов. Именно так наш файл сайта попадает в общий конфиг: в реальном /etc/nginx/nginx.conf внутри http есть строка include /etc/nginx/sites-enabled/*; («подключи все файлы из этого каталога»).
Каталоги sites-available и sites-enabled. Это соглашение Ubuntu и Debian, не нужное самому nginx:
- в
/etc/nginx/sites-available/лежат все написанные конфиги сайтов, включённые и нет; - в
/etc/nginx/sites-enabled/лежат только симлинки на них. Симлинк (symbolic link, «символическая ссылка») это файл-указатель: он не хранит содержимое, а ссылается на другой файл, как ярлык на рабочем столе. Создаётся командойln -s ЦЕЛЬ ИМЯ_ССЫЛКИ.
Зачем два каталога? Чтобы выключить сайт, достаточно удалить симлинк (rm удалит только ссылку, файл в sites-available останется), а чтобы включить снова, создать симлинк. Ничего не теряется. nginx подключает всё содержимое sites-enabled, а sites-available он не читает.
Сразу после установки в sites-enabled лежит один симлинк:
lrwxrwxrwx 1 root root 34 Sep 30 11:52 default -> /etc/nginx/sites-available/default
Первая буква l значит «ссылка» (link), а стрелка -> показывает, куда она ведёт. Файл default объявляет сайт, который по любому запросу отдаёт страницу «Welcome to nginx!». Убрать её нужно, иначе она будет мешать нашему сайту (почему, объясним в следующем разделе).
Осторожно: sites-available не «включает» сайт сам по себе, файл, на который нет симлинка в sites-enabled, для nginx не существует. Если открыть в редакторе файл через симлинк, правится настоящий файл в sites-available, и это нормально. Общий конфиг nginx можно просмотреть целиком, уже со всеми подключёнными файлами, командой sudo nginx -T (заглавная T: как -t, но ещё печатает результат). Это первое, что нужно сделать, когда «конфиг вроде правильный, а работает не так»: ты увидишь то, что nginx действительно загрузил.
Прикинь сам: файл лежит в
sites-available/, но ссылки на него вsites-enabled/нет. Применится ли он?
Нет: nginx читает только sites-enabled/. Включение это ссылка (ln -s) из sites-available/.
Главное: конфиг это директивы и блоки, а активные сайты те, что лежат в
sites-enabled/.
Проверь понимание: ты положил
notes.confв/etc/nginx/sites-available/, выполнилnginx -tиreload, но сайт не появился. Что забыто?
Ответ
Симлинк в /etc/nginx/sites-enabled/: sudo ln -s /etc/nginx/sites-available/notes /etc/nginx/sites-enabled/notes. nginx подключает только sites-enabled, а файл в sites-available он не читает.
Файлы подключены, но на одном порту может жить много сайтов. Дальше разберём, как nginx из них выбирает нужный.
Как nginx выбирает server: listen и server_name
На одном сервере и одном порту 80 может жить много сайтов: notes.lab, shop.lab, grafana.lab. Все клиенты приходят на один IP и один порт. Чтобы понять, чей это запрос, nginx смотрит на заголовок Host внутри запроса (урок 2.4): там клиент написал, к какому имени он обращается.
Представь один подъезд, на домофоне много квартир. Ты звонишь по номеру квартиры (заголовку Host), а входишь в один и тот же подъезд.
Как выбирается блок server.
- По адресу и порту (
listen) находятся всеserver, которые слушают этот порт. - Среди них ищется тот, у которого имя в
server_nameсовпадает с заголовкомHostзапроса. - Если совпадения нет, запрос отдаётся серверу по умолчанию для этого порта. Им становится либо тот, где стоит пометка
default_server(listen 80 default_server;), либо, если пометок нет, первый по порядку в конфиге.
Пока в sites-enabled лежал только default (с пометкой default_server, имя _), любое имя открывало страницу «Welcome to nginx!». Когда я включил и наш notes, и default, произошло вот что:
$ curl -s -H 'Host: notes.lab' http://127.0.0.1/ -> «Заметки» (имя совпало с server_name notes.lab)
$ curl -s -H 'Host: shop.lab' http://127.0.0.1/ -> <title>Welcome to nginx!</title> (имени shop.lab нет, ушло на default_server)
А когда default выключен, единственный оставшийся сервер становится сервером по умолчанию сам, и ему достаются любые имена:
$ curl -s -H 'Host: shop.lab' http://127.0.0.1/ -> Notes service vdev
Чтобы не гадать, кто ответит, читай sudo nginx -T | grep -n server_name: там видно все имена.
Как читать имя default_server и _. Имя _ не что-то особенное: это просто недействительное имя, которое ни с чем не совпадёт. Так принято писать «сервер для всех остальных».
Осторожно: server_name notes.lab не создаёт DNS-запись. Имя notes.lab должно быть отдельно описано в /etc/hosts или DNS (урок 2.3), иначе клиент даже не узнает адрес, а server_name только помогает nginx выбрать блок. Ответ «Welcome to nginx!» вместо приложения почти всегда значит, что запрос попал на дефолтный сайт: Host не совпал или дефолтный сайт не выключен. А два блока с одним и тем же именем на одном порту конфликтуют: nginx предупредит conflicting server name "notes.lab" on 0.0.0.0:80, ignored и второй блок просто проигнорирует.
Прикинь сам: клиент пришёл с
Host: shop.lab, а в конфиге есть толькоnotes.lab. Какой сервер ответит?
Единственный, он становится сервером по умолчанию и получает любые имена.
Главное: сервер выбирается по
listenи заголовкуHost, без совпадения ответит сервер по умолчанию.
Проверь понимание: в конфиге один
serverсserver_name notes.lab;, другого нет. Клиент отправил запрос сHost: shop.labна этот же сервер и порт 80. Кто ответит?
Ответ
Ответит блок notes.lab. Совпадающего имени нет, но других серверов на порту тоже нет, и единственный становится сервером по умолчанию. Значит, «нет server_name» не защищает от чужих имён.
Сайт выбран, но внутри него разным путям нужны разные правила. Как nginx выбирает среди них, разбираем дальше.
Как nginx выбирает location
Внутри одного server разным путям нужны разные правила: / отдать приложению, /static/ отдать файлы с диска, /healthz ответить самому. Блоки location описывают правила для групп путей.
Что такое путь. Путь (URI-путь, урок 2.4) это часть адреса после имени сайта и до знака ?. В http://notes.lab/slow?sec=5 путь это /slow.
Как записываются location. После слова location идёт необязательный модификатор (спецсимвол), потом образец, потом блок:
| Запись | Что значит | Название |
|---|---|---|
location = /healthz { } |
путь равен ровно /healthz, ни символом больше |
точное совпадение |
location ^~ /static/ { } |
путь начинается с /static/ и дальше регулярки не проверять |
приоритетный префикс |
location ~ \.txt$ { } |
путь подходит под регулярное выражение (образец текста, как в grep, урок 1.2): здесь «кончается на .txt» |
регулярка (с учётом регистра) |
location ~* \.txt$ { } |
то же, но A и a считаются равными |
регулярка без учёта регистра |
location /docs/ { } |
путь начинается с /docs/ |
обычный префикс |
Порядок выбора. Запрос попадает ровно в один location. nginx проверяет так:
- Ищет точное совпадение (
=). Нашёл: берёт его и больше не смотрит. - Иначе выбирает самый длинный префикс среди всех
locationбез регулярок (и с^~). Запоминает его. Если это^~, то на этом всё: регулярки не проверяются. - Иначе идёт по регуляркам сверху вниз в порядке записи в файле. Первая подошедшая побеждает.
- Если ни одна регулярка не подошла, берётся запомненный префикс.
Так что регулярка побеждает обычный префикс, даже более длинный. Это неочевидно, разберём на примере.
flowchart TD
Q["Путь запроса"] --> E{"Есть точное<br>совпадение =?"}
E -->|"да"| R1["Берём его"]
E -->|"нет"| P["Выбираем самый длинный префикс"]
P --> H{"Это префикс ^~ ?"}
H -->|"да"| R2["Берём его, регулярки не смотрим"]
H -->|"нет"| G{"Подошла регулярка<br>(сверху вниз)?"}
G -->|"да"| R3["Берём первую подошедшую"]
G -->|"нет"| R4["Берём запомненный префикс"]
Регулярка проверяется только после выбора префикса и перебивает его, если префикс не помечен ^~.
Чтобы не гадать, я создал отдельный server на порту 8088 с пятью правилами и спросил у него семь путей:
location / { return 200 "1: prefix /\n"; }
location /docs/ { return 200 "2: prefix /docs/\n"; }
location = /healthz { return 200 "3: exact =/healthz\n"; }
location ~ \.txt$ { return 200 "4: regex .txt\n"; }
location ^~ /static/ { return 200 "5: ^~ /static/\n"; }
(Директива return 200 "текст"; значит «ответь сам кодом 200 с этим текстом, приложению не передавай».) Результаты:
/healthz -> 3: exact =/healthz (шаг 1: точное совпадение)
/healthz/ -> 1: prefix / (косая черта в конце: уже не равно точно, подошёл только «/»)
/docs/a -> 2: prefix /docs/ (самый длинный префикс, регулярка не подошла)
/docs/a.txt -> 4: regex .txt (регулярка побеждает префикс /docs/, хоть он и длиннее)
/static/a.txt -> 5: ^~ /static/ (у префикса стоит ^~, регулярки не проверялись)
/x.txt -> 4: regex .txt (префикс только «/», регулярка подошла)
/other -> 1: prefix / (ничего сильнее не подошло)
Строки /docs/a.txt и /static/a.txt дают разный результат потому, что во втором случае у префикса стоит ^~.
Осторожно: «побеждает самый длинный» верно только для префиксов. Регулярка длиннее не становится, она просто выигрывает у префикса, а между собой регулярки решает порядок записи. Ещё /healthz и /healthz/ это разные пути, и точное = /healthz второй не ловит. Порядок блоков в файле важен только между регулярками, префиксы можно записать в любом порядке.
Прикинь сам: есть
location /docs/иlocation ~ \.txt$. Куда попадёт/docs/a.txt?
В регулярку: она проверяется после префикса и побеждает его, если префикс без ^~.
Главное: запрос попадает ровно в один
location: точный, потом префикс^~, потом регулярки по порядку, потом обычный префикс.
Проверь понимание: есть
location /иlocation = /healthz. В какой блок попадётGET /healthzи в какойGET /healthz/?
Ответ
/healthz попадёт в точный = /healthz. /healthz/ не равен точному пути, поэтому попадёт в location /.
Нужный location найден, и теперь остаётся главное: передать запрос приложению. Это делает proxy_pass, и у него есть мелочь, на которой часто ломаются.
proxy_pass: передать запрос приложению
Директива proxy_pass внутри location и есть «позвонить в номер»: она говорит, на какой адрес отправить запрос. Без неё nginx попытался бы искать файл на диске.
Значение это адрес приложения: proxy_pass http://127.0.0.1:8080;. http:// это схема (урок 2.4), дальше адрес и порт приложения. Что nginx отправит приложению в строке запроса (пути), зависит от одной мелочи: есть ли путь после порта.
proxy_pass http://127.0.0.1:8080;(пути нет, косой черты тоже). Путь запроса передаётся как есть.proxy_pass http://127.0.0.1:8080/;(после порта стоит/, это уже «путь»). Та часть пути, которая совпала сlocation, вырезается и заменяется этим путём.
Это как почтовый адрес с припиской «передайте в кабинет 5»: в первом случае письмо передают дальше как есть, во втором на ходу переклеивают адрес.
Разницу видно сразу. Я поднял server на порту 8089 с двумя location и спросил у обоих одно и то же:
location /api/ { proxy_pass http://127.0.0.1:8080/; } # со слэшем
location /raw/ { proxy_pass http://127.0.0.1:8080; } # без
$ curl -s http://127.0.0.1:8089/api/healthz
ok # приложение получило /healthz: префикс /api/ вырезан
$ curl -s http://127.0.0.1:8089/raw/healthz
{"error": "not found"} # приложение получило /raw/healthz: пути такого у него нет
Оба варианта корректны, если ты знаешь, что нужно приложению. Беда возникает, когда слэш добавляют или убирают «для красоты», и приложение начинает получать другие пути. В нашем notes.conf используется location / без пути, и вопроса нет: путь передаётся как есть.
Осторожно: слэш в конце proxy_pass не косметика. Симптом в жизни: часть страниц отвечает 404 сразу после «мелкой правки». Проверить, какой путь получило приложение, можно по его журналу (там виден запрос) или по эндпоинту, который печатает ответ.
Прикинь сам: ты добавил слэш в конце
proxy_pass«для красоты». Что может сломаться?
Приложение начнёт получать другие пути: часть пути, совпавшая с location, вырежется, и страницы дадут 404.
Главное: слэш в конце
proxy_passменяет путь, который получит приложение.
Проверь понимание:
location /api/ { proxy_pass http://127.0.0.1:8080; }. Какой путь получит приложение при запросе/api/notes? А если написатьproxy_pass http://127.0.0.1:8080/;?
Ответ
Без слэша приложение получит /api/notes (путь как есть). Со слэшем получит /notes: префикс /api/ вырезан и заменён на /.
Запрос дошёл до приложения, но приложение видит в нём только nginx, а не клиента. Как вернуть ему эти сведения, расскажет следующий раздел.
Заголовки X-Forwarded-*: как приложение узнаёт настоящего клиента
Когда запрос идёт через nginx, приложение видит: «подключился 127.0.0.1». Реального клиента оно не знает. Без этого невозможны честные логи («кто пришёл»), ограничения по IP, а приложение не может узнать, что пользователь пришёл по HTTPS, а не по HTTP. Поэтому прокси сообщает это в дополнительных заголовках (урок 2.4).
Что за заголовки.
Host: имя, которое клиент набрал (notes.lab). Без нашей директивы nginx подставил бы туда адрес приложения127.0.0.1:8080, и приложение не узнало бы, как к нему обратились. Подставляется значением переменной$host.X-Real-IP: адрес клиента, каким его увидел nginx. Берётся из$remote_addr.X-Forwarded-For: цепочка адресов. Клиент мог сам пройти через другой прокси; каждый прокси дописывает в конец списка тот адрес, от которого получил запрос. Значение переменной$proxy_add_x_forwarded_forэто то, что клиент прислал в этом заголовке, плюс$remote_addrчерез запятую (а если заголовка не было, просто$remote_addr).X-Forwarded-Proto: по какому протоколу клиент пришёл,httpилиhttps(переменная$scheme). Между nginx и приложением всегда простой HTTP, а клиент мог зайти по HTTPS (урок 2.6). Приложению нужно знать об этом, чтобы строить правильные ссылки и перенаправления.
Переменные nginx. Слово с долларом ($host, $remote_addr) это переменная nginx, значение которой он подставляет в момент запроса. Не путай с переменными bash: оболочка их не видит, пока они внутри файла конфига.
Директива proxy_set_header. Она устанавливает заголовок, который nginx отправит приложению: proxy_set_header ИМЯ ЗНАЧЕНИЕ;.
Четыре запроса на /headers, эндпоинт которого возвращает то, что получило приложение.
Простой запрос с того же сервера. Клиент и nginx на одной машине, $remote_addr равен 127.0.0.1:
{"Host": "notes.lab", "X-Real-IP": "127.0.0.1", "X-Forwarded-For": "127.0.0.1", "X-Forwarded-Proto": "http", "Connection": "close", ...}
Тот же запрос, но клиент сам написал в X-Forwarded-For: 1.2.3.4 (подделал):
{"Host": "notes.lab", "X-Real-IP": "127.0.0.1", "X-Forwarded-For": "1.2.3.4, 127.0.0.1", ...}
Вот поэтому важно: цепочка растёт справа, и слева стоит то, что написал клиент, а 127.0.0.1 в конце дописал наш nginx. Верить можно только последним адресам, добавленным вашим прокси, а левые записи может подделать кто угодно. Приложение не должно слепо брать первый адрес из X-Forwarded-For, если клиент может обратиться напрямую.
Запрос с другого адреса (из сети, через адрес сетевой карты 172.17.0.4 моего стенда):
{"Host": "notes.lab", "X-Real-IP": "172.17.0.4", "X-Forwarded-For": "172.17.0.4", ...}
Приложение видит настоящий адрес клиента, хотя само общается только с nginx.
Если убрать строку proxy_set_header Host $host;, nginx по умолчанию подставит в Host адрес приложения:
{"X-Real-IP": "127.0.0.1", "X-Forwarded-For": "127.0.0.1", "X-Forwarded-Proto": "http", "Host": "127.0.0.1:8080", "Connection": "close", ...}
Осторожно: X-Real-IP и X-Forwarded-For не стандарт (X- значит «нестандартный»), а общая договорённость. Приложение обязано само их читать, автоматически ничего не подменится. И заголовкам можно верить, только если запрос пришёл от вашего nginx. Поэтому приложение и слушает только 127.0.0.1: снаружи до него не добраться в обход nginx (см. раздел ниже).
Прикинь сам: клиент прислал
X-Forwarded-For: 1.2.3.4. Чему верить в итоговой цепочке?
Только адресам, которые дописал твой nginx, то есть правым. Левые написал клиент, он мог соврать.
Главное:
X-Forwarded-*передают приложению настоящего клиента и схему, но подделываются, если прокси им слепо верит.
Проверь понимание: зачем приложению
X-Forwarded-Proto, если оно всегда слушает обычный HTTP?
Ответ
Между nginx и приложением всегда HTTP, а клиент мог прийти по HTTPS (урок 2.6). Из заголовка приложение узнаёт исходную схему и строит правильные ссылки и перенаправления.
Теперь запрос идёт через nginx целиком, и когда что-то сломается, нужно знать, куда смотреть. Об этом следующий раздел: о логах.
Логи nginx: access.log и error.log
Нужно два лога: один отвечает на вопрос «что происходило» (каждый запрос), другой на вопрос «что пошло не так» (ошибки самого nginx). Путать их нельзя: диагностика 502 и 504 держится на error.log.
Куда пишутся. Директивы access_log и error_log задают файлы. В нашем notes.conf это /var/log/nginx/notes-access.log и /var/log/nginx/notes-error.log. Читать их может root и группа adm, поэтому нужен sudo. Общий /var/log/nginx/error.log это лог всего nginx (например, ошибок запуска), а не только твоего сайта.
access.log: строка на запрос. Формат по умолчанию называется combined. Разберём реальную строку с моего стенда:
127.0.0.1 - - [30/Sep/2026:11:53:43 +0000] "GET /nope HTTP/1.1" 404 22 "-" "curl/8.5.0"
| Часть строки | Что это | Номер поля при разбиении по пробелам |
|---|---|---|
127.0.0.1 |
адрес клиента ($remote_addr) |
1 |
- - |
два поля, обычно пустые (имя пользователя из старых схем аутентификации) | 2, 3 |
[30/Sep/2026:11:53:43 +0000] |
дата, время и часовой пояс запроса. Скобки часть строки | 4 и 5 (пробел внутри скобок делит её на две части) |
"GET /nope HTTP/1.1" |
строка запроса: метод, путь, версия. Кавычки часть строки | 6, 7, 8 |
404 |
код ответа | 9 |
22 |
размер тела ответа в байтах | 10 |
"-" |
откуда пришёл запрос (Referer), тут «нет» | 11 |
"curl/8.5.0" |
кто спрашивал (User-Agent) | 12 |
Поэтому awk '{print $9}' вытаскивает код ответа, а awk '{print $7}' путь. Разбиение awk идёт по пробелам, и дата, содержащая пробел, «съедает» два поля: вот откуда девятка, а не седьмое.
error.log: что не получилось. Строка формата «время, уровень, номер процесса, сообщение и хвост из подробностей». Настоящая строка при остановленном приложении:
2026/09/30 11:53:44 [error] 681#681: *35 connect() failed (111: Connection refused) while connecting to upstream, client: 127.0.0.1, server: notes.lab, request: "GET / HTTP/1.1", upstream: "http://127.0.0.1:8080/", host: "notes.lab"
[error]: уровень важности (warn,error,emergв порядке нарастания);681#681: номер процесса worker;*35: номер соединения внутри nginx: по нему связывают строки одного запроса;connect() failed (111: Connection refused) while connecting to upstream: причина.111 Connection refusedэто тот же отказ из урока 2.2, который возвращает ядро, когда на порту никто не слушает;client:,server:,request:,upstream:,host:: кто спрашивал, какой блокserverпринял, что просили, куда пытался передать, какое имяHostбыло в запросе. Полеupstream:показывает, какому приложению nginx звонил (это важно: адрес нужно сверять сnotes.env).
Расширенный формат с временем. По умолчанию в логе нет времени обработки. Его добавляют директивой log_format, которая описывает свой формат из переменных ($request_time время всего запроса в секундах, $upstream_response_time время, которое ответило приложение), и вторым аргументом директивы access_log:
log_format timed '$remote_addr "$request" $status rt=$request_time urt=$upstream_response_time';
access_log /var/log/nginx/notes-access.log timed;
Я проверил это на стенде: запросы /slow?sec=2 и / записались так:
127.0.0.1 "GET /slow?sec=2 HTTP/1.1" 200 rt=2.003 urt=2.003
127.0.0.1 "GET / HTTP/1.1" 200 rt=0.000 urt=0.000
Так видно, какие пути тормозят и виноват ли upstream (urt близко к rt) или сам nginx.
Осторожно: не считай 500 в access.log признаком «сломался nginx». Код 500 обычно приложение вернуло само (например, наш /error), nginx лишь передал его, и в error.log для него записи не будет. И не ищи причину 502 в access.log: там только код 502 и размер, а причина в error.log.
Прикинь сам: пользователь жалуется на ошибку, а в
access.logпусто. Где искать?
В error.log: запрос мог не дойти до записи (обрыв, отказ), или проблема в самом nginx.
Главное:
access.logотвечает «что происходило»,error.log«что пошло не так».
Проверь понимание: в
access.logстрока с кодом200и размером22, а вerror.logпусто. Ошибся ли nginx?
Ответ
Нет. 200 значит, что запрос успешно обслужен, а error.log пишет только сбои nginx. Пустой error.log при нормальной работе это норма.
Логи читать умеем. Теперь применим это к двум самым частым ошибкам прокси, 502 и 504, и научимся отличать их по логам.
Коды 502 и 504: кто их формирует и как отличить
Коды 5xx (ошибки на стороне сервера, урок 2.4) бывают трёх видов: приложение вернуло само (500), nginx сообщает, что не смог достучаться до приложения (502, 504), или nginx сам решил отказать (например, 503 из return). Причина у каждого своя, а искать нужно в разных местах.
Администратор звонит в номер. Если трубку никто не берёт и гудков нет вообще (линия отключена), он говорит гостю «номер недоступен» (502). Если гудки идут, трубку сняли, но человек молчит и молчит, через минуту администратор сдаётся: «номер не отвечает» (504). Если в номере ответили «у нас пожар» (500), это ответ номера, а не администратора.
Кто какой код формирует, сведено в таблицу:
| Код | Кто формирует | Что случилось | Что в error.log |
|---|---|---|---|
| 500 | приложение (nginx только передал) | приложение упало внутри обработчика или так задумано (/error) |
ничего: для nginx это обычный ответ |
| 502 Bad Gateway | nginx | не удалось получить нормальный ответ: порт закрыт, процесс остановлен, соединение оборвано | connect() failed (111: Connection refused) while connecting to upstream |
| 504 Gateway Timeout | nginx | соединение с приложением есть, но ответа нет дольше proxy_read_timeout (по умолчанию 60 секунд; в нашем конфиге 30) |
upstream timed out (110: Connection timed out) while reading response header from upstream |
| 503 и др. | nginx по правилу в конфиге | например, return 503 в location |
ничего: nginx отказал сознательно |
Таймауты. Два, которые есть в нашем notes.conf:
proxy_connect_timeout 3s;: сколько ждать, пока приложение примет соединение. Если порт открыт, но приложение «не берёт трубку» (очередь переполнена), через 3 секунды будет 504 с текстом...while connecting to upstream. При закрытом порте отказ приходит мгновенно и таймаут не нужен;proxy_read_timeout 30s;: сколько ждать ответа после отправки запроса. Считается не общее время запроса, а время молчания между двумя порциями данных.
Три запроса подряд показывают разницу, время замерено.
Приложение остановлено (sudo systemctl stop notes). Ответ пришёл за real 0m0.003s, то есть мгновенно, и это признак 502: порт закрыт, ядро сразу сказало «отказ». В логе connect() failed (111: Connection refused) (строка целиком разобрана выше).
Приложение работает, но запрос /slow?sec=40 требует спать 40 секунд, а proxy_read_timeout 30:
504
real 0m30.041s
2026/09/30 11:54:15 [error] 682#682: *37 upstream timed out (110: Connection timed out) while reading response header from upstream, client: 127.0.0.1, server: notes.lab, request: "GET /slow?sec=40 HTTP/1.1", upstream: "http://127.0.0.1:8080/slow?sec=40", host: "notes.lab"
504 пришёл ровно через 30 секунд, а не через 40. То есть по времени ответа уже видно, что сработал таймаут.
А что стало с самим приложением? Оно доработало запрос: через 40 секунд с начала оно пыталось отправить ответ, но nginx уже закрыл соединение, и в журнале journalctl -u notes появилась запись:
python3[825]: 2026-09-30 11:54:25,097 INFO 127.0.0.1 "GET /slow?sec=40 HTTP/1.0" 200 -
python3[825]: Exception occurred during processing of request from ('127.0.0.1', 47090)
...
python3[825]: BrokenPipeError: [Errno 32] Broken pipe
Broken pipe («разорванная труба») значит «писал в соединение, которое другая сторона уже закрыла». Сервис при этом жив, ошибка относится только к этому запросу. Вывод: 504 не останавливает медленную работу приложения, nginx просто перестал ждать.
sequenceDiagram
participant C as Клиент
participant N as nginx
participant A as Приложение
C->>N: запрос
N->>A: подключиться
alt порт закрыт
A-->>N: отказ сразу
N->>C: 502 за миллисекунды
else приложение молчит дольше proxy_read_timeout
N->>C: 504 через 30 секунд
end
Скорость ответа с ошибкой подсказывает причину: мгновенный отказ это 502, ответ ровно через таймаут это 504.
Осторожно: 502 и 504 не значат, что nginx сломался, он как раз исправно сообщает, что с приложением проблема. Не поднимай proxy_read_timeout до 300 секунд, чтобы «504 пропали»: это маскировка, а не лечение, клиент по-прежнему ждёт пять минут, а приложение по-прежнему медленное. Законно увеличивать таймаут только для тех путей, где долгая работа осознанная (выгрузка отчёта), и только для их location. И заглядывай в Server:: заголовок Server: nginx/... в ответе с ошибкой говорит, что страницу ошибки написал nginx, а если его нет, ошибку сформировал кто-то другой.
Прикинь сам: клиент ждал ровно 30 секунд и получил ошибку. Это 502 или 504?
504: время ожидания совпало с proxy_read_timeout. Мгновенный отказ был бы 502.
Главное: 502 это приложение недоступно, 504 приложение не ответило вовремя. Оба формирует nginx.
Проверь понимание: приложение остановлено. Какой код увидит клиент, за какое время и какая строка появится в
error.log?
Ответ
502, практически мгновенно. В error.log: connect() failed (111: Connection refused) while connecting to upstream. Порт закрыт, ядро сразу отвечает отказом (урок 2.2).
Мы разобрали, как nginx сообщает о сбоях приложения. Остался вопрос, как он справляется с медленными клиентами и почему приложению с ним легче.
Что ещё nginx делает по дороге и зачем это приложению
Простой Python-сервер из app.py обслуживает клиента, пока тот не получит ответ целиком. Если клиент на медленной мобильной сети читает ответ десять секунд, поток приложения все десять секунд занят. Тысяча таких клиентов, и потоки закончились: приложение не отвечает даже быстрым. Прокси решает это тем, что разговаривает с медленным клиентом сам.
Курьер (приложение) отдаёт коробку администратору за секунду и едет дальше. А администратор (nginx) сколько угодно долго объясняет гостю, куда идти и что получить. Аналогия перестаёт работать в одном: администратор хранит коробку у себя в памяти, а память конечна.
Эта особенность называется буферизацией (buffering, «накопление в промежуточной памяти»), и в nginx она включена по умолчанию:
- Приложение отвечает nginx быстро, по соединению между ними (второе соединение из первого раздела).
- nginx складывает ответ в свою память (при большом размере во временный файл на диске).
- Приложение освобождается сразу и закрывает соединение (вот почему nginx просит
Connection: close). - nginx отдаёт накопленное клиенту в том темпе, в каком клиент его принимает.
Похожая работа идёт и в обратную сторону: nginx сначала принимает запрос от клиента целиком и только потом звонит приложению. Приложение не тратит время на «ожидание, пока клиент допишет запрос».
Из этого вытекает ещё одно. Nginx каждый запрос обслуживает не отдельным потоком, а событийной моделью (event-driven): один worker следит сразу за тысячами соединений и занимается тем, у кого в этот момент есть данные. Поэтому нескольких worker (по одному на ядро) хватает на десятки тысяч подключений. Про потоки приложения этого не скажешь.
Цифры из журнала приложения и nginx для запроса /slow?sec=5:
приложение: "GET /slow?sec=5 HTTP/1.0" 200 - <- протокол HTTP/1.0: закрыть соединение после ответа
nginx: "GET /slow?sec=5 HTTP/1.1" 200 7 <- клиенту ответ HTTP/1.1, 7 байт (slept 5)
Слева видно, что с приложением nginx говорит по упрощённому HTTP/1.0 с Connection: close, а клиенту отвечает по HTTP/1.1 с Connection: keep-alive («не закрывай, пригодится для следующих запросов»). То есть nginx переводит между двумя диалектами: приложению так проще, клиенту так быстрее.
Осторожно: буферизация не делает медленное приложение быстрым, если приложение думает 40 секунд, клиент ждёт эти 40 секунд. И nginx не создаёт по потоку на запрос, именно этим он и отличается от простых серверов. Ещё ответ, который нужно отдавать частями сразу (например, потоковую передачу), буферизация задерживает. Для таких путей её отключают директивой proxy_buffering off;. У «Заметок» таких путей нет, и мы её не трогаем.
Прикинь сам: приложение отвечает медленно, поможет ли буферизация nginx его ускорить?
Нет: клиент всё равно ждёт столько же. Буфер лишь освобождает приложение от медленного клиента.
Главное: nginx разгружает приложение, но не ускоряет его.
Проверь понимание: зачем nginx закрывает соединение с приложением сразу после ответа, хотя клиенту ещё отдаёт данные?
Ответ
Чтобы освободить приложение. Nginx уже забрал ответ в буфер и сам доставит его клиенту, какой бы медленной ни была его сеть. Занятым остаётся дешёвый worker nginx, а не поток приложения.
Буферизация лишь одна из работ, которые nginx берёт на себя. Какие ещё задачи можно вынести на него, смотрим дальше.
Три роли nginx: что ещё перед приложением
Мы использовали nginx только как «передатчик». Но приложению можно вынести на nginx ещё несколько задач, поэтому важно знать, чем ещё он занимается и какие из них появятся в курсе позже.
Вернёмся к администратору гостиницы. У него, кроме переговоров с номерами, есть и другие обязанности: проверять документы у входа, отдавать ключи и справочники самому, не будя номера, следить, чтобы у стойки не выстраивалась чрезмерная очередь.
В роли, которые nginx может выполнять перед приложением, входят:
- Шифрование (TLS). Nginx принимает зашифрованное соединение на порту 443, расшифровывает и передаёт приложению обычный HTTP. Приложению не нужно ничего знать о сертификатах (урок 2.6). Именно поэтому нужен заголовок
X-Forwarded-Proto. - Раздача статических файлов. Картинки, стили, скрипты не меняются, и nginx отдаёт их сам прямо с диска (директива
rootилиaliasвнутриlocation), приложение к этому не привлекается. - Балансировка. Когда приложений несколько, в блоке
upstream { server ...; server ...; }перечисляют все, и nginx распределяет между ними запросы. В курсе она появится позже вместе с Kubernetes. - Ограничение нагрузки. Nginx умеет ограничивать число запросов с одного адреса за секунду (
limit_req) и размер тела запроса (client_max_body_size, по умолчанию 1 МБ).
Ограничение размера тела легко проверить: по умолчанию client_max_body_size равен 1 МБ, и запрос больше него получает от nginx код 413 Request Entity Too Large (по-русски «слишком большое тело запроса»). Приложение об этом запросе не узнает вообще, и в его журнале пусто, зато в error.log nginx будет строка client intended to send too large body. Это ещё один пример ответа, который сформировал nginx, а не приложение (как 502 и 504). Мы этот код на стенде не воспроизводили, значение по умолчанию взято из документации nginx.
Осторожно: всё это не нужно настраивать сразу. notes.conf в этом уроке содержит только проксирование, заголовки и таймауты. Остальное добавляется, когда появляется потребность.
Прикинь сам: нужно ли сразу настраивать TLS, балансировку и лимиты?
Нет. Эти роли добавляют, когда появляется потребность. В нашем notes.conf только проксирование, заголовки и таймауты.
Главное: nginx шифрует, раздаёт файлы, балансирует и ограничивает нагрузку, а приложению остаётся своя работа.
Проверь понимание: запрос вернул 413, а в журнале приложения его нет. Ошибка в приложении или в nginx?
Ответ
Ответил nginx: тело запроса превысило client_max_body_size, и приложение до запроса не дошло. Причину ищем в error.log nginx и в конфиге, а не в приложении.
Остаётся последний вопрос: как сделать, чтобы все эти возможности нельзя было обойти. Для этого приложение прячут за 127.0.0.1.
Почему приложению остаётся 127.0.0.1
Мы поставили nginx для того, чтобы все запросы проходили через него. Если приложение слушает 0.0.0.0:8080, любой в сети может обратиться к порту 8080 напрямую и обойти nginx со всеми его заголовками, логами и (позже) TLS. Приложение будет считать «настоящим клиентом» кого угодно, потому что доверяет заголовкам X-Forwarded-*.
Адрес привязки HOST=127.0.0.1 (урок 2.1) значит, что порт 8080 открыт только на loopback: пакеты, пришедшие с сетевой карты, до него не доходят, потому что адрес получателя другой. А nginx слушает 0.0.0.0:80 и доступен с любого адреса. Порт 8080 закрывается не файрволом, а самой привязкой:
flowchart LR
C1["Клиент снаружи"] -->|"172.17.0.4:80"| N["nginx"]
N -->|"127.0.0.1:8080"| A["«Заметки»"]
C1 -.->|"172.17.0.4:8080: отказ"| X["на этом адресе порт 8080<br>никто не слушает"]
Прямой путь к порту 8080 снаружи закрыт: единственная дорога к приложению идёт через nginx.
Запрос на адрес сетевой карты (172.17.0.4, у тебя будет свой):
$ curl -s -H 'Host: notes.lab' http://172.17.0.4/headers
{"Host": "notes.lab", "X-Real-IP": "172.17.0.4", ...} # через nginx работает
$ curl -sS --max-time 3 http://172.17.0.4:8080/healthz
curl: (7) Failed to connect to 172.17.0.4 port 8080 after 0 ms: Couldn't connect to server # напрямую нет
Осторожно: 127.0.0.1 это не «пароль на порт», а адрес, доступный только с самой машины. Если сервис нужно защитить и от локальных пользователей, потребуются права и другие механизмы. Файрвол (урок 2.7) это следующий уровень защиты, а не замена.
Прикинь сам: что случится, если приложение слушает
0.0.0.0:8080?
Любой клиент сможет обратиться к порту 8080 напрямую и обойти nginx с его заголовками и логами.
Главное:
127.0.0.1оставляет единственный путь к приложению через прокси.
Проверь понимание: приложение слушает
127.0.0.1:8080, nginx на0.0.0.0:80. Достучится ли клиент из другой сети до порта 8080 напрямую?
Ответ
Нет. 127.0.0.1 доступен только с самой машины (урок 2.1): пакет с внешним адресом получателя до него не дойдёт, клиент получит отказ (Connection refused). Открыт только порт 80, и запрос идёт через nginx.
Практика
Все задания выполняются на твоей ВМ с Ubuntu 24.04 или 26.04 (урок 1.1). Ожидается, что после прошлых уроков есть:
- сервис
notes.service(Заметки на127.0.0.1:8080) из урока 1.8, приложение версии v3 из урока 2.4 (проверь:curl -s http://127.0.0.1:8080/headersвозвращает JSON, а неnot found); - запись
127.0.0.1 notes.labв/etc/hosts(урок 2.3); - репозиторий-каталог
~/notesс проектом.
Выводы в уроке получены на Ubuntu 24.04 с nginx 1.24.0. Номера процессов, даты, адреса, число worker и время у тебя будут другими.
Задание 1. Установка nginx и разбор дефолтной конфигурации
Цель: поставить nginx, увидеть страницу по умолчанию и найти, откуда она берётся.
Предскажи: сколько процессов nginx запустится и какой порт они займут? Что вернёт curl -sI http://127.0.0.1/?
Ответ
Один master от root и несколько worker от www-data (worker_processes auto, по числу ядер), порт 80. curl -sI вернёт HTTP/1.1 200 OK и заголовок Server: nginx/....
Шаги:
-
Убедись, что порт 80 свободен, и поставь nginx из репозитория Ubuntu. Разбор:
ss -ltn 'sport = :80'показывает слушающие TCP-сокеты (-lслушающие,-tTCP,-nчислами, урок 2.2), а в кавычках стоит фильтр «порт источника равен 80»;apt install -yставит пакет без вопросов (урок 1.1);systemctl is-activeпечатает состояние сервиса.ss -ltn 'sport = :80' sudo apt update sudo apt install -y nginx systemctl is-active nginxПакет сам запускает nginx при установке. Если
is-activeпечатаетinactive(так было в моём Docker-стенде), запусти вручную:sudo systemctl start nginx. -
Посмотри процессы и порт. Разбор:
psпоказывает процессы;-C nginxвыбирает те, у которых команда называетсяnginx;-o pid,user,cmdзадаёт колонки (номер, владелец, команда):ps -o pid,user,cmd -C nginx ss -ltn 'sport = :80' -
Проверь ответ и найди, какой конфиг его отдаёт. У
curlфлаг-s(silent) отключает индикатор загрузки, флаг-Iотправляет запросHEADи показывает только заголовки (урок 2.4). Вторая команда берёт из страницы строку с заголовком:curl -sI http://127.0.0.1/ curl -s http://127.0.0.1/ | grep title ls -l /etc/nginx/sites-enabled/
Что должно получиться:
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
active
PID USER CMD
431 root nginx: master process /usr/sbin/nginx -g daemon on; master_process on;
432 www-data nginx: worker process
433 www-data nginx: worker process
... (у меня 15 строк worker, у тебя по числу ядер)
State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0 511 0.0.0.0:80 0.0.0.0:*
HTTP/1.1 200 OK
Server: nginx/1.24.0 (Ubuntu)
Date: Wed, 30 Sep 2026 11:52:35 GMT
Content-Type: text/html
Content-Length: 615
Last-Modified: Wed, 30 Sep 2026 11:52:27 GMT
Connection: keep-alive
ETag: "6abcf7fb-267"
Accept-Ranges: bytes
<title>Welcome to nginx!</title>
lrwxrwxrwx 1 root root 34 Sep 30 11:52 default -> /etc/nginx/sites-available/default
Первая строка State ... это заголовок таблицы первого ss (порт свободен, строк нет). Если у тебя на порту 80 уже кто-то слушает, остановись и освободи порт (см. ошибки ниже).
Как читать вывод:
ps: первая строкаroot ... nginx: master processэто master, остальныеwww-data ... worker processэто worker. Число worker равно числу ядер.LISTEN 0 511 0.0.0.0:80: порт 80 слушают на всех адресах.511это длина очереди ожидающих подключений (первая колонка 0 значит, что сейчас очередь пуста). В настоящем выводе могут быть ещё строка для[::]:80: это то же самое для IPv6.HTTP/1.1 200 OKиServer: nginx/1.24.0 (Ubuntu): ответил nginx именно этой версии. На Ubuntu 26.04 будет версия 1.28.Content-Length: 615иContent-Type: text/html: страница на 615 байт, это HTML-файл/var/www/html/index.nginx-debian.html.default -> /etc/nginx/sites-available/default: единственный сайт включён симлинком на файлdefault, он и отдаёт страницу.
Объясни себе:
- Почему master работает от root, а worker от
www-data? (подсказка: порт 80 меньше 1024, урок 1.3) - Что даст удаление симлинка
sites-enabled/default?
Типичные ошибки:
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use): порт 80 занят другим процессом (Apache, другой веб-сервер, или чужой контейнер: изолированная «коробка» с программой, урок 4.1. Найди, кто:sudo ss -ltnp 'sport = :80'(флаг-pпоказывает процесс), и останови его.curl: (7) Failed to connect to 127.0.0.1 port 80 after 0 ms: Couldn't connect to server: nginx не запущен.sudo systemctl start nginx, потомjournalctl -u nginx -n 20. На Ubuntu 26.04 текст будетCould not connect to server, смысл тот же.E: Unable to locate package nginx: не выполненsudo apt update.
Задание 2. Конфиг reverse proxy для «Заметок»
Цель: написать deploy/nginx/notes.conf в репозитории, подключить его в nginx и получить ответ приложения на порту 80.
Предскажи: что покажет curl http://notes.lab/headers: адрес клиента 127.0.0.1 в X-Real-IP или пустое значение? Почему Host будет notes.lab, а не 127.0.0.1:8080?
Ответ
X-Real-IP: 127.0.0.1, если ты запрашиваешь с того же сервера (nginx видит клиента 127.0.0.1). Host будет notes.lab, потому что мы передаём $host (исходное имя из запроса), а не подставляем адрес приложения.
Шаги:
-
Создай файл конфига в репозитории. Команда
cat > ФАЙЛ <<'CONF'записывает в файл всё, что напечатано до строкиCONF(тот приём, что был в уроке 1.8). Кавычки вокруг'CONF'обязательны: без них bash сам подставил бы$hostи$remote_addrв текст (они пустые), и в файле остались бы дыры. Можно и открыть файл вvimилиnanoи вставить текст.mkdir -p ~/notes/deploy/nginx cat > ~/notes/deploy/nginx/notes.conf <<'CONF' # Прокси перед «Заметками»: nginx :80 -> приложение 127.0.0.1:8080 server { listen 80; server_name notes.lab; access_log /var/log/nginx/notes-access.log; error_log /var/log/nginx/notes-error.log; location / { proxy_pass http://127.0.0.1:8080; # Приложение за прокси не видит клиента, поэтому передаём явно 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; proxy_connect_timeout 3s; proxy_read_timeout 30s; } } CONFЕсли ты вставляешь текст в блок
catс отступом (как в этом уроке), убери лишние пробелы слева, чтобы в файлеserverначинался с первой колонки. Лишние пробелы nginx стерпит, но читать конфиг будет неудобно.Разбор файла по строкам:
server { ... }: один «сайт»;listen 80;: принимать соединения на порту 80 (на всех адресах IPv4);server_name notes.lab;: этот блок отвечает на запросы с заголовкомHost: notes.lab;access_logиerror_log: отдельные логи этого сайта в/var/log/nginx/(по ним удобно искать, они не смешиваются с чужими);location / { ... }: правило для всех путей (обычный префикс/подходит любому пути, и без другихlocationон единственный);proxy_pass http://127.0.0.1:8080;: передавать запрос приложению, путь как есть (без слэша после порта);- четыре
proxy_set_header: дописать заголовки, о которых шла речь в теории; proxy_connect_timeout 3s;: ждать соединения с приложением не дольше 3 секунд;proxy_read_timeout 30s;: ждать ответа не дольше 30 секунд (по умолчанию 60).
-
Установи конфиг, включи сайт, выключи дефолтный и проверь синтаксис. Разбор:
cpкопирует файл;ln -s ЦЕЛЬ ИМЯсоздаёт симлинк (там, где nginx ищет включённые сайты);rmна симлинк удаляет только ссылку, файлdefaultвsites-availableостаётся;nginx -tпроверяет конфиг и ничего не применяет.sudo cp ~/notes/deploy/nginx/notes.conf /etc/nginx/sites-available/notes sudo ln -s /etc/nginx/sites-available/notes /etc/nginx/sites-enabled/notes sudo rm /etc/nginx/sites-enabled/default sudo nginx -t -
Примени без разрыва соединений и проверь. Флаг
-iуcurlдобавляет к ответу заголовки, а-sубирает индикатор загрузки:sudo systemctl reload nginx curl -si http://notes.lab/ curl -s http://notes.lab/headers
Что должно получиться:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
HTTP/1.1 200 OK
Server: nginx/1.24.0 (Ubuntu)
Date: Wed, 30 Sep 2026 11:52:42 GMT
Content-Type: text/plain; charset=utf-8
Content-Length: 19
Connection: keep-alive
Notes service vdev
{"Host": "notes.lab", "X-Real-IP": "127.0.0.1", "X-Forwarded-For": "127.0.0.1", "X-Forwarded-Proto": "http", "Connection": "close", "User-Agent": "curl/8.5.0", "Accept": "*/*"}
Строка Notes service vdev зависит от APP_VERSION в /etc/notes/notes.env (там dev, потому что приложению печатается v плюс значение). Порядок ключей в JSON у тебя может отличаться.
Как читать вывод:
- Две строки
nginx: ... is okи... is successful: конфиг разобран без ошибок. Если строки нет, а есть[emerg], читай текст ошибки (номер строки указан в конце). HTTP/1.1 200 OKиServer: nginx/1.24.0: ответ прошёл через nginx.Content-Length: 19это длинаNotes service vdevс переводом строки.- В
/headersпришли четыре заголовка, которые мы задали (Host,X-Real-IP,X-Forwarded-For,X-Forwarded-Proto).Connection: closeи заголовокUser-Agentдобавил nginx или передал клиент. X-Real-IP: 127.0.0.1, потому что ты запрашиваешь с этой же машины.
Объясни себе:
- Чем
$remote_addrотличается от$proxy_add_x_forwarded_for? (вспомни разбор с подделкой заголовка в теории) - Почему сначала
nginx -t, а потомreload? - Что изменится в
/headers, если удалить строкуproxy_set_header Host $host;? (по умолчанию nginx подставитHost: 127.0.0.1:8080, проверь)
Типичные ошибки:
2026/09/30 11:53:24 [emerg] 755#755: unknown directive "proxy_pas" in /etc/nginx/sites-enabled/notes:10и следомnginx: configuration file /etc/nginx/nginx.conf test failed: опечатка в имени директивы на 10-й строке. Исправь файл и сноваnginx -t. Если ты запустишьsystemctl reload nginxс такой ошибкой, получишьJob for nginx.service failed., а сайт продолжит работать по старому конфигу.[emerg] "server" directive is not allowed here in /etc/nginx/sites-enabled/notes:9:serverоказался внутри другого блока, обычно потому что где-то выше не закрыта скобка. Проверь{и}.[emerg] unexpected end of file, expecting "}"(не закрыта скобка) или[emerg] unexpected "}"(лишняя): смотри номер строки и парность скобок.curl: (6) Could not resolve host: notes.lab: нет записи в/etc/hosts(урок 2.3):echo '127.0.0.1 notes.lab' | sudo tee -a /etc/hosts.ln: failed to create symbolic link '/etc/nginx/sites-enabled/notes': File exists: симлинк уже есть, всё в порядке. Проверьls -l /etc/nginx/sites-enabled/.- После
reloadсразу пришёл ответ по старому конфигу (например, страница «Welcome to nginx!» или 404 от nginx):reloadасинхронный, новые worker ещё поднимались. Повтори запрос через секунду. [warn] conflicting server name "notes.lab" on 0.0.0.0:80, ignored: два файла вsites-enabledобъявляют одно имя. Оставь один.- Вместо «Заметок» открывается «Welcome to nginx!»: дефолтный сайт не выключен или
server_nameне совпал сHost. Смотриsudo nginx -T | grep -n server_name.
Нейросеть может объяснить строку лога и конфиг, но не видит твой сервер. Убери адреса и имена, а потом найди названную ею директиву или строку в своих файлах: если её нет, нейросеть её придумала.
Задание 3. Читаем access.log и разбираем формат
Цель: по логу отвечать на вопросы «кто, что и как быстро».
Предскажи: сколько строк добавится в notes-access.log после трёх запросов curl? Какой код будет у GET /nope?
Ответ
Три строки, по одной на запрос. /nope даст 404: такого пути в приложении нет, оно отвечает 404 (урок 2.4). Код пришёл от приложения, а не от nginx: nginx его передал как есть.
Шаги:
-
Очисти логи, чтобы старые запросы не мешали счёту.
truncate -s 0 ФАЙЛобнуляет файл, не удаляя его. Затем сделай три запроса. Уcurlфлаг-o /dev/nullвыбрасывает тело ответа (специальный файл/dev/nullэто «чёрная дыра»), а-sубирает индикатор:sudo truncate -s 0 /var/log/nginx/notes-access.log /var/log/nginx/notes-error.log curl -s -o /dev/null http://notes.lab/ curl -s -o /dev/null http://notes.lab/nope curl -s -o /dev/null http://notes.lab/error -
Прочитай последние строки и посчитай коды (приёмы из урока 1.2). Разбор второй команды:
awk '{print $9}'печатает девятое поле каждой строки (код ответа),sortупорядочивает,uniq -cсчитает подряд идущие одинаковые строки,sort -rnставит большие числа наверх.sudo tail -n 3 /var/log/nginx/notes-access.log sudo awk '{print $9}' /var/log/nginx/notes-access.log | sort | uniq -c | sort -rn -
Посчитай долю ошибок 5xx. Разбор:
$9 ~ /^5/значит «девятое поле начинается с 5»;e++увеличивает счётчик;NRэто число прочитанных строк; блокENDвыполняется после последней строки;printfпечатает по формату (%dцелое,%.1fчисло с одним знаком после точки,%%символ процента).sudo awk '$9 ~ /^5/ {e++} END {printf "%d из %d, %.1f%%\n", e, NR, e*100/NR}' /var/log/nginx/notes-access.log
Что должно получиться:
127.0.0.1 - - [30/Sep/2026:11:53:43 +0000] "GET / HTTP/1.1" 200 19 "-" "curl/8.5.0"
127.0.0.1 - - [30/Sep/2026:11:53:43 +0000] "GET /nope HTTP/1.1" 404 22 "-" "curl/8.5.0"
127.0.0.1 - - [30/Sep/2026:11:53:43 +0000] "GET /error HTTP/1.1" 500 21 "-" "curl/8.5.0"
1 500
1 404
1 200
1 из 3, 33.3%
Как читать вывод:
- Каждая строка это один запрос. Разбор полей в таблице в теории:
127.0.0.1клиент, в скобках время,"GET /nope HTTP/1.1"запрос,404код,22размер тела в байтах ({"error": "not found"}как раз 22 символа), в конце User-Agent. - Для
/errorкод 500 и размер 21 принесло приложение ({"error":"synthetic"}), nginx его только передал. - Три строки счёта означают «по одному ответу каждого кода». Формат
uniq -c: число повторов, затем значение. - Последняя строка:
1ошибка 5xx из3запросов, 33.3%.
Версия curl и время у тебя другие. Девятое поле по пробелам это код ответа, потому что дата в скобках содержит пробел и занимает два поля.
Объясни себе:
- Как отличить 500, который вернуло приложение, от 502, которое сформировал nginx? (подсказка: у 502 есть запись в
error.log, у 500 нет) - Как найти самый частый путь среди ошибок 5xx одной командой? (подсказка:
awk '$9 ~ /^5/ {print $7}'и дальше знакомый конвейер)
Типичные ошибки:
tail: cannot open '/var/log/nginx/notes-access.log' for reading: Permission denied: логи читают root и группаadm, забытsudo.awkпечатает не код, а путь или пустоту: ты взял не то поле. Проверь на одной строке, какое поле что. Код ответа$9, путь$7.awk: ... division by zero: лог пуст (только что очищен, запросов ещё не было). Сначала сделай запросы.
Задание 4. 502 и 504 своими руками
Цель: вызвать оба кода и найти причину в error.log, а не гадать.
Предскажи: какой код вернёт nginx, если остановить notes.service? А если приложение работает, но /slow?sec=40 при proxy_read_timeout 30s?
Ответ
Остановленное приложение: 502 сразу (Connection refused). /slow?sec=40: 504 через 30 секунд (upstream timed out). Разница во времени ответа тоже диагностический признак.
Шаги:
-
- Останови приложение, сделай запрос и прочитай
error.log, потом запусти приложение обратно:
sudo systemctl stop notes curl -si http://notes.lab/ sudo tail -n 1 /var/log/nginx/notes-error.log sudo systemctl start notes - Останови приложение, сделай запрос и прочитай
-
- Замерь время, запрос будет висеть 30 секунд. Разбор:
time КОМАНДАпечатает, сколько она выполнялась (строкаreal);-w '%{http_code}\n'уcurlпечатает после ответа только код;-o /dev/nullвыбрасывает тело:
time curl -s -o /dev/null -w '%{http_code}\n' 'http://notes.lab/slow?sec=40' sudo tail -n 1 /var/log/nginx/notes-error.log - Замерь время, запрос будет висеть 30 секунд. Разбор:
-
Проверь, что при
sec=5всё в порядке (5 секунд меньше 30):curl -s 'http://notes.lab/slow?sec=5' -
Подожди ещё секунд десять (пока пройдут 40 секунд с начала шага 2) и посмотри журнал приложения.
journalctl -u notesпоказывает журнал сервиса (урок 1.8),-n 12последние 12 строк,--no-pagerне открывает постраничный просмотр:sudo journalctl -u notes -n 12 --no-pager
Что должно получиться:
HTTP/1.1 502 Bad Gateway
Server: nginx/1.24.0 (Ubuntu)
Date: Wed, 30 Sep 2026 11:53:44 GMT
Content-Type: text/html
Content-Length: 166
Connection: keep-alive
<html>
<head><title>502 Bad Gateway</title></head>
<body>
<center><h1>502 Bad Gateway</h1></center>
<hr><center>nginx/1.24.0 (Ubuntu)</center>
</body>
</html>
2026/09/30 11:53:44 [error] 681#681: *35 connect() failed (111: Connection refused) while connecting to upstream, client: 127.0.0.1, server: notes.lab, request: "GET / HTTP/1.1", upstream: "http://127.0.0.1:8080/", host: "notes.lab"
504
real 0m30.041s
user 0m0.002s
sys 0m0.002s
2026/09/30 11:54:15 [error] 682#682: *37 upstream timed out (110: Connection timed out) while reading response header from upstream, client: 127.0.0.1, server: notes.lab, request: "GET /slow?sec=40 HTTP/1.1", upstream: "http://127.0.0.1:8080/slow?sec=40", host: "notes.lab"
slept 5
Sep 30 11:54:25 e88127068a3d python3[825]: 2026-09-30 11:54:25,097 INFO 127.0.0.1 "GET /slow?sec=40 HTTP/1.0" 200 -
Sep 30 11:54:25 e88127068a3d python3[825]: Exception occurred during processing of request from ('127.0.0.1', 47090)
Sep 30 11:54:25 e88127068a3d python3[825]: self._sock.sendall(b)
Sep 30 11:54:25 e88127068a3d python3[825]: BrokenPipeError: [Errno 32] Broken pipe
В последнем блоке я оставил самое важное: журнал длиннее, между строками идёт трассировка Python.
Как читать вывод:
- 502: тело ошибки (
<title>502 Bad Gateway</title>) написал сам nginx, поэтому в конце страницы стоит его имя и версия. Ответ пришёл мгновенно (Content-Length: 166это размер этой страницы). - Строка
error.logпроConnection refused: причина названа буквально.upstream: "http://127.0.0.1:8080/"показывает, куда nginx пытался дозвониться. - Для 504:
real 0m30.041sэто время ожидания, ровноproxy_read_timeout.userиsysэто процессорное время самойcurl, почти ноль: она просто ждала. - Строка
upstream timed out (110: Connection timed out) while reading response header from upstream: «соединение было, но заголовок ответа не пришёл вовремя». slept 5: приложение сsec=5уложилось в 30 секунд и ответило.- В журнале приложения: запрос
/slow?sec=40завершился с кодом 200 на 40-й секунде (в логеHTTP/1.0: nginx говорит с приложением по HTTP/1.0). Клиенту уже отправили 504, поэтому при записи ответаBrokenPipeError.
Объясни себе:
- Почему 504 пришёл ровно через 30 секунд, а не через 40?
- Что произошло с приложением после 504? (проверь в журнале: оно доработало запрос, но не смогло отправить ответ)
- Поднять
proxy_read_timeoutдо 120 секунд: лечение или маскировка? Когда какое?
Типичные ошибки:
curl: (28) Operation timed out after ...: сработал ограничительcurl(--max-time), а не nginx. Сравни с 504: там ответ приходит, и в нём код.- Нужной строки нет в
error.logпосле 504: ты смотришь в общий/var/log/nginx/error.log, а сайт пишет в свой/var/log/nginx/notes-error.log(директиваerror_logвнутриserver). - После
systemctl start notesсразу 502: приложение стартует за долю секунды, подожди секунду и повтори. - Вместо 502 при остановленном приложении открылась страница «Welcome to nginx!»: перед этим ты не выключил
default(задание 2).
Задание 5. Шаг проекта: «Заметки» за nginx
Цель: привести проект к состоянию после урока 2.5 и убедиться, что порт 8080 закрыт от внешнего мира, а 80 открыт.
Предскажи: приложение слушает 127.0.0.1:8080, а nginx на 0.0.0.0:80. Достучится ли внешний клиент до :8080 напрямую?
Ответ
Нет. 127.0.0.1 доступен только с самой машины (урок 2.1). Снаружи открыт только 80, и запрос идёт через nginx. Порт 8080 закрывается не файрволом, а привязкой к loopback.
Шаги:
-
Убедись, что в
/etc/notes/notes.envстоитHOST=127.0.0.1и приложение слушает именно этот адрес.grep ^HOSTпечатает строки, которые начинаются сHOST(^это «начало строки»). Файл читает только root и группаnotes, поэтомуsudo:sudo grep ^HOST /etc/notes/notes.env ss -ltn 'sport = :8080' -
Убедись, что файл в репозитории и установленный конфиг совпадают.
diff A Bпечатает различия и ничего, если файлы равны, а&& echo sameпечатаетsameтолько при успехе:diff ~/notes/deploy/nginx/notes.conf /etc/nginx/sites-available/notes && echo same -
Проверь весь путь, включая POST через прокси. Флаги
curl:-X POSTзадаёт метод,-Hдобавляет заголовок,-dтело запроса (урок 2.4):curl -s -X POST -H 'Content-Type: application/json' -d '{"text":"через nginx"}' http://notes.lab/notes curl -s http://notes.lab/notes -
Проверь снаружи: возьми адрес сетевой карты и обратись к нему. Разбор первой строки тот же, что в уроке 2.1:
ip -4 -br a show scope globalпечатает IPv4-адреса,awkберёт третью колонку,cutотрезает маску. Флаг-H 'Host: notes.lab'подставляет имя вручную (для адреса-цифры в/etc/hostsзаписи нет),--max-time 3ограничивает ожидание:IP=$(ip -4 -br a show scope global | awk '{print $3; exit}' | cut -d/ -f1) echo "$IP" curl -s -H 'Host: notes.lab' "http://$IP/headers" curl -sS --max-time 3 "http://$IP:8080/healthz" -
Убедись, что nginx стартует при загрузке:
systemctl is-enabled nginx -
Закоммить конфиг:
cd ~/notes && git add deploy/nginx/notes.conf && git commit -m "Добавлен nginx reverse proxy"
Что должно получиться:
HOST=127.0.0.1
State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0 5 127.0.0.1:8080 0.0.0.0:*
same
{"id": 1}
[{"id": 1, "text": "через nginx", "created_at": "2026-09-30T11:54:37+00:00"}]
172.17.0.4
{"Host": "notes.lab", "X-Real-IP": "172.17.0.4", "X-Forwarded-For": "172.17.0.4", "X-Forwarded-Proto": "http", "Connection": "close", "User-Agent": "curl/8.5.0", "Accept": "*/*"}
curl: (7) Failed to connect to 172.17.0.4 port 8080 after 0 ms: Couldn't connect to server
enabled
Если git в проекте ещё не подключён (урок 3.1), на шестом шаге будет fatal: not a git repository: пропусти его, коммит появится позже.
Как читать вывод:
HOST=127.0.0.1и127.0.0.1:8080в колонкеLocal Address: приложение слушает только loopback.same: файл в репозитории идентичен установленному,diffничего не нашёл.{"id": 1}: приложение создало заметку с номером 1 (код 201). Пробел после двоеточия ставит Python, для JSON он значения не имеет. Если в твоём хранилище уже были заметки,idбудет больше.created_atэто время создания в UTC: у тебя оно другое.- Адрес
172.17.0.4(у тебя свой): через него nginx ответил, и вX-Real-IPтот самый адрес клиента. Приложение теперь видит настоящего отправителя. Failed to connect ... port 8080 ... Couldn't connect to server: напрямую на 8080 не достучаться. Это то, что нужно. На 26.04 curl напишетCould not connect to server.
Объясни себе:
- Что увидит приложение в
X-Forwarded-For, когда запрос придёт с другой машины? А если клиент сам подделает этот заголовок? - Почему конфиг хранится в репозитории, а не только в
/etc/nginx? - Что произойдёт, если в
notes.envпоставитьHOST=0.0.0.0? (подсказка: порт 8080 станет доступен в обход nginx)
Типичные ошибки:
[warn] conflicting server name "notes.lab" on 0.0.0.0:80, ignored: два файла вsites-enabledобъявляют одно имя: оставь один.- Вместо «Заметок» открывается страница «Welcome to nginx!»: дефолтный сайт не выключен или
server_nameне совпал сHost. Проверьsudo nginx -T | grep -n server_name. curl: (7) Failed to connect to 172.17.0.4 port 80: nginx слушаетlisten 80;на IPv4-адресах, а ты пробуешь IPv6 (::1) или nginx остановлен. Проверьss -ltn 'sport = :80'иsystemctl is-active nginx.fatal: not a git repository (or any of the parent directories): .git: репозитория пока нет (урок 3.1), см. выше.
Сломай и почини
Скачай сценарий и запусти один из четырёх случаев, не читая скрипт: диагностика и есть упражнение. Скрипт меняет конфиг nginx для notes.lab и настройки «Заметок», поэтому запускай его только на своей учебной ВМ. У curl флаг -f значит «при ошибке сервера не сохраняй страницу с ошибкой», -L разрешает переходы по перенаправлениям, -o задаёт имя файла:
curl -fsSL -o /tmp/break-2.5.sh https://raw.githubusercontent.com/distinguished-sre/learning/main/devops/project/notes/break/2.5/break.sh
sudo bash /tmp/break-2.5.sh 1
Сценарии: 1, 2, 3 и 4. Проходи по одному: запусти, найди причину, почини сам или командой sudo bash /tmp/break-2.5.sh fix, потом бери следующий. Новый сценарий скрипт не запустит, пока не снят прошлый. В конце выполни fix и удали скрипт: rm /tmp/break-2.5.sh. Скрипт проверяет, что уроки 1.8 и 2.5 (задания 1 и 2) выполнены: сервис notes, nginx и файл /etc/nginx/sites-available/notes должны быть на месте.
Симптом
- Сценарий 1. Ты правил конфиг и выполнил
reload. Скрипт напечатал ошибку. Сайт продолжает отвечать, ноsudo nginx -tне проходит. - Сценарий 2.
systemctl is-active notesотвечаетactive, ноcurl -si http://notes.lab/мгновенно возвращает502 Bad Gateway. - Сценарий 3.
curl -si http://notes.lab/висит около 30 секунд и заканчивается504 Gateway Timeout.systemctl status notesпри этом в порядке. - Сценарий 4.
curl -si http://notes.lab/healthzвозвращает503, аcurl -s http://127.0.0.1:8080/healthzотвечаетok.
Гипотезы
Прежде чем что-то чинить, выпиши 2-3 версии для своего симптома. Для этих симптомов подходят:
- В конфиге nginx ошибка синтаксиса или он не применён.
- Приложение остановлено, слушает другой порт или не тот адрес.
- Upstream принимает соединение, но не отвечает (таймаут).
- Запрос попал не в тот
location, либо nginx сам отдал ошибку.
Подумай, что подтверждает мгновенный отказ, а что ожидание в 30 секунд. Таблица кодов из теории подскажет.
Проверки
Иди от простого к сложному, одна проверка на гипотезу:
sudo nginx -t # 1: синтаксис и номер строки с ошибкой
curl -si http://notes.lab/ # 2-4: код и заголовок Server
sudo tail -n 5 /var/log/nginx/notes-error.log # 2-3: причина в тексте и адрес upstream
systemctl is-active notes # 2: жив ли сервис
sudo ss -ltnp 'sport = :8080' # 2: слушает ли приложение порт 8080
grep '^PORT=' /etc/notes/notes.env # 2: что записано в настройках
time curl -s -o /dev/null -w '%{http_code}\n' http://notes.lab/ # 2-3: 502 сразу, 504 после таймаута
sudo nginx -T | grep -n 'location\|proxy_pass' # 4: какие location реально загружены и куда идёт proxy_pass
sudo ss -ltnp # 3: кто ещё слушает порты
ss -ltnp печатает слушающие сокеты и процессы. Загляни также в sudo tail -n 3 /var/log/nginx/notes-access.log: там видно код и размер ответа.
Разбор и исправление
Сценарий 1. Синтаксическая ошибка. nginx -t печатает [emerg] unknown directive "proxy_read_timeuot" in /etc/nginx/sites-enabled/notes:19: опечатка в имени директивы. Пока -t не проходит, reload не применит конфиг, сайт продолжает работать по прошлому: поломка ждёт следующего restart или перезагрузки сервера, а тогда nginx не поднимется вовсе. Исправление: открыть sudo nano /etc/nginx/sites-available/notes, поправить proxy_read_timeout на 19-й строке, повторить sudo nginx -t, затем sudo systemctl reload nginx.
Сценарий 2. 502. В error.log: connect() failed (111: Connection refused) while connecting to upstream ... upstream: "http://127.0.0.1:8080/". При этом systemctl is-active notes даёт active, а ss -ltnp 'sport = :8080' пуст. Виновата пара из grep '^PORT=' /etc/notes/notes.env (там PORT=8090) и ss -ltnp 'sport = :8090' (там слушает python): приложение живо, но на другом порту, чем ждёт nginx. Как и в уроке 2.1, быстрый отказ значит «пакет дошёл, но по этому адресу и порту никого нет». Исправление: вернуть порт и перезапустить.
sudo sed -i 's/^PORT=.*/PORT=8080/' /etc/notes/notes.env
sudo systemctl restart notes
Сценарий 3. 504. В error.log: upstream timed out (110: Connection timed out) while reading response header from upstream, ... upstream: "http://127.0.0.1:8081/". Обрати внимание на порт 8081: в notes.conf должно быть 8080. sudo ss -ltnp 'sport = :8081' покажет процесс python, который принимает соединения и молчит: соединение установлено, ответа нет, так что nginx ждёт положенные 30 секунд. Исправление: в /etc/nginx/sites-available/notes вернуть proxy_pass http://127.0.0.1:8080;, sudo nginx -t, sudo systemctl reload nginx. Зависший процесс остановит sudo systemctl stop notes-break-hang (его создал скрипт). Общая мысль: если запрос долго висит на законной причине (тяжёлый отчёт), чини приложение, а если тормозить должно, поднимай proxy_read_timeout только для нужного location.
Сценарий 4. Приоритет location. nginx -T | grep -n location показывает два новых правила перед location /: location /healthz (обычный префикс, отвечает ok) и location ~ ^/health (регулярка, отвечает 503 maintenance). Запрос /healthz подходит под оба, но по порядку выбора из теории регулярка побеждает обычный префикс, поэтому отвечает 503 (в error.log пусто, в access.log код 503 и размер 12). Поэтому на 127.0.0.1:8080 всё в порядке. Исправление: убрать оба временных блока (или сделать нужный точным: location = /healthz, точное совпадение побеждает регулярку), затем sudo nginx -t и sudo systemctl reload nginx.
Общий вывод: сначала код и заголовок Server, затем error.log, затем состояние upstream. Перезапуск ничего не объясняет.
Быстрый способ снять любую поломку: sudo bash /tmp/break-2.5.sh fix.
ИИ в помощь
Нейросеть хорошо объясняет директивы и строки логов, но не знает, как устроен твой сервер, и охотно советует «поднять таймаут». Общие правила работы с ней: ИИ-помощник.
Задача: разобрать строки логов.
Я учу nginx. Вот строки из notes-access.log и notes-error.log (адреса я заменил):
<вставь 3-5 строк каждого файла>.
Объясни по полям, что просил клиент и что ответил nginx, и назови, кто сформировал код: nginx или приложение.
Проверь ответ: сверь с разбором формата combined и таблицей кодов 500, 502, 504. Типичная ошибка нейросети: называет 502 ошибкой в nginx, хотя он лишь сообщает о проблеме с приложением.
Задача: проверить конфиг перед применением.
Вот мой notes.conf: <вставь файл целиком>.
Найди ошибки и риски: выбор server и location, слэш в proxy_pass, заголовки X-Forwarded-*, таймауты.
Для каждого замечания назови, как это проверить командой. Файл не переписывай.
Проверь ответ: запусти sudo nginx -t и проверь замечания по одному. Типичная ошибка: советует поднять proxy_read_timeout до 300 секунд «чтобы пропали 504», а это маскировка.
Задача: понять, почему запрос попал не туда.
В конфиге такие location: <вставь список>. Запрос идёт на путь <путь>.
Покажи по шагам, какой location выберет nginx, и объясни порядок: точное, префикс ^~, регулярки, обычный префикс.
Проверь ответ: проверь через curl -H 'Host: ...' на тестовом server. Типичная ошибка: считает, что побеждает самый длинный префикс, хотя регулярка его перебивает.
Словарик урока
| Термин | Простыми словами |
|---|---|
| Обратный прокси (reverse proxy) | Программа перед приложением: принимает запросы клиентов и передаёт их приложению. Клиент приложение не видит |
| nginx | Самый распространённый веб-сервер и обратный прокси |
| upstream | «Вышестоящий сервер»: приложение, куда nginx передаёт запросы. У нас 127.0.0.1:8080 |
| master и worker | Главный процесс nginx (читает конфиг, управляет) и рабочие (обслуживают запросы) |
| reload | Применить новый конфиг без обрыва соединений: новые worker берут новый конфиг, старые дорабатывают |
| restart | Полный перезапуск: соединения рвутся |
nginx -t |
Проверка синтаксиса конфига без применения |
nginx -T |
Проверка и печать всего загруженного конфига, со всеми подключёнными файлами |
| Директива | Одна настройка вида «имя значение;» |
| Блок, контекст | Группа директив в { }: http, server, location |
include |
Подключить содержимое других файлов на место директивы |
sites-available и sites-enabled |
Каталоги Ubuntu: в первом все конфиги сайтов, во втором симлинки на включённые |
| Симлинк (symbolic link) | Файл-указатель на другой файл, как ярлык. Создаётся ln -s |
server |
Блок «сайт»: порт (listen) и имя (server_name) |
server_name |
Имя, по которому nginx выбирает server для запроса (сверяется с заголовком Host) |
default_server |
Сервер, который получает запросы с неизвестным именем |
location |
Блок правила для группы путей |
Точное совпадение = |
location = /path: путь равен ровно этому значению |
Префикс ^~ |
Совпадение по началу пути, регулярки после него не проверяются |
Регулярка ~ и ~* |
Образец текста; побеждает обычный префикс. Между регулярками решает порядок в файле |
proxy_pass |
Передать запрос на адрес приложения |
proxy_set_header |
Установить заголовок, который nginx отправит приложению |
Host |
Заголовок с именем сайта, к которому обращается клиент |
X-Real-IP |
Адрес клиента, каким его увидел nginx |
X-Forwarded-For |
Цепочка адресов: каждый прокси дописывает адрес, от которого получил запрос |
X-Forwarded-Proto |
Как клиент пришёл: http или https |
$host, $remote_addr, $scheme |
Переменные nginx: имя, адрес клиента, схема запроса |
proxy_connect_timeout |
Сколько ждать соединения с приложением |
proxy_read_timeout |
Сколько ждать ответа приложения (по умолчанию 60 с) |
| 502 Bad Gateway | nginx не получил нормальный ответ: порт закрыт или соединение оборвано |
| 504 Gateway Timeout | Соединение есть, но приложение не ответило за proxy_read_timeout |
access.log |
Журнал запросов: строка на запрос |
error.log |
Журнал ошибок nginx: здесь причина 502 и 504 |
$request_time |
Общее время обработки запроса в секундах, доступно в log_format |
| Прокси (proxy) | Посредник: принимает запрос от имени другого и передаёт дальше. Прямой стоит у клиента, обратный у сервера |
| Трафик | Данные, которые ходят по сети |
| API | Адрес и правила, по которым одна программа обращается к другой |
| Очередь | Порядок ожидания, когда клиентов больше, чем сервер успевает обслужить |
| Поток (thread) | Одна «рука» программы: обслуживает одного клиента за раз |
| Уязвимость | Ошибка в программе, которой может воспользоваться злоумышленник |
| Broken pipe | Ошибка записи в соединение, которое другая сторона уже закрыла |
Вопросы с собеседований
Раздел для повторения: ответь вслух, потом открой ответ. Короткие вопросы с пометкой [на скорость] тренируй на время: ответ за 30 секунд.
1. [junior] [часто] Что такое reverse proxy и зачем он перед приложением?
Ответ
Reverse proxy принимает запросы клиентов и передаёт их приложению за ним, а ответ возвращает клиенту. Клиент видит только nginx. Он даёт единую точку входа для нескольких сервисов, терминацию TLS, балансировку между бэкендами (upstream), буферизацию медленных клиентов, логи и лимиты. Приложение слушает 127.0.0.1:8080, а nginx слушает 80 и 443 и делает proxy_pass http://127.0.0.1:8080;. Настоящий адрес клиента передаю заголовком X-Forwarded-For. Forward proxy наоборот стоит перед клиентами и ходит в интернет за них.
Что хотят услышать: что это посредник перед приложением, терминация TLS, балансировка, proxy_pass, передача реального IP, отличие от forward proxy
Красный флаг: путать reverse proxy с VPN или не знать, откуда приложение берёт адрес клиента
2. [junior] [часто] Прод отвечает 502 Bad Gateway. Твои действия?
Ответ
Проверяю, что 502 отдал именно nginx (curl -i, заголовок Server), открываю error.log и читаю причину. Обычно Connection refused: приложение остановлено или слушает другой порт. Смотрю systemctl status, ss -ltn, журнал приложения. Поднимаю сервис и ищу, почему он упал.
Что хотят услышать: порядок «код, error.log, состояние upstream», знание, что 502 это ответ nginx, а не приложения, упоминание ss и journalctl.
Красный флаг: «перезапустил nginx» без чтения логов.
3. [junior] [часто] Ты поправил конфиг nginx на проде. Как применить безопасно?
Ответ
Сначала копия старого файла, потом правка, затем nginx -t, и только после него systemctl reload nginx. После reload проверяю curl и error.log. Reload не рвёт текущие соединения (старые worker дорабатывают запросы), restart рвёт. Если проверка не прошла, применять нельзя: возвращаю файл из копии.
Что хотят услышать: nginx -t до применения, разница reload и restart, откат из копии.
Красный флаг: правка на проде и сразу restart без проверки синтаксиса.
4. [junior] [на скорость] Чем 502 отличается от 504 и как понять по времени ответа?
Ответ
502: nginx не получил осмысленный ответ, обычно отказ соединения, ответ приходит сразу. 504: соединение есть, но приложение молчит дольше proxy_read_timeout, ответ приходит после таймаута. Причина 504 почти всегда в медленном приложении или его зависимости (например, база данных или внешний сервис).
Что хотят услышать: connect() failed против upstream timed out, таймауты proxy_connect_timeout и proxy_read_timeout.
Красный флаг: «оба значит сервер лёг».
5. [junior] Приложение за nginx видит у всех клиентов адрес 127.0.0.1. Как получить настоящий?
Ответ
Прокси должен передавать адрес заголовками X-Real-IP и X-Forwarded-For (proxy_set_header ... $remote_addr и $proxy_add_x_forwarded_for), а приложение читать их. Доверять нужно только заголовкам от своего прокси: клиент может сам прислать X-Forwarded-For, и его запись окажется в начале цепочки. Надёжен адрес, дописанный своим nginx (последний).
Что хотят услышать: proxy_set_header, $remote_addr, $proxy_add_x_forwarded_for, проблема доверия к заголовку.
Красный флаг: «в IP клиента из X-Forwarded-For можно всегда верить».
6. [middle] Сайт открывается медленно, в логе nginx много 504 по одному пути. Как разбираешься?
Ответ
Считаю долю 504 и находлю путь по access.log (awk по коду и пути). Добавляю в log_format время запроса $request_time и время ответа приложения $upstream_response_time, смотрю, где тратится время. Проверяю приложение и его зависимости в эти минуты. Затем решаю, что чинить: запрос, базу или ресурсы. Увеличиваю proxy_read_timeout только осознанно и только для этого location.
Что хотят услышать: $request_time, $upstream_response_time, локализация по пути, разница между лечением и маскировкой.
Красный флаг: «подниму таймаут до 600 секунд».
7. [middle] После деплоя часть запросов даёт 502, а потом всё проходит. В чём может быть дело?
Ответ
Приложение перезапускается и короткое время не слушает порт, поэтому в эти секунды nginx получает Connection refused. Проверяю журнал (journalctl) на рестарты и совпадение времени с 502. Решение: выкатывать без простоя (позже в курсе: Kubernetes, урок 5.7) или на время перезапуска держать второй экземпляр приложения.
Что хотят услышать: корреляция времени 502 с рестартами, понимание окна недоступности, идея плавной выкатки.
Красный флаг: «это nginx глючит».
8. [middle] curl на /healthz возвращает не то, что ожидалось, хотя правило для него есть. Что проверишь?
Ответ
Смотрю загруженный конфиг nginx -T: возможно, другой блок перехватывает запрос. Напоминаю порядок: точное =, потом самый длинный префикс (^~ останавливает поиск), потом регулярки по порядку записи, и только потом запомненный префикс. Регулярка побеждает обычный префикс даже более длинный. Ещё проверяю, что путь запрошен ровно так: /healthz/ не равно = /healthz. И не перехватывает ли server_name другой файл в sites-enabled.
Что хотят услышать: nginx -T, порядок выбора location, конфликт server_name.
Красный флаг: «в nginx приоритет всегда зависит от порядка строк».
9. [middle] Что такое proxy_pass со слэшем и без в конце? Приведи пример беды.
Ответ
Для location /api/ { proxy_pass http://app:8080; } (без слэша) запрос /api/notes уйдёт приложению как есть. Для proxy_pass http://app:8080/; (со слэшем) префикс /api/ вырезается, и приложение получит /notes. Беда: кто-то «для красоты» добавил слэш, и приложение получает другие пути, отвечает 404. Проверяю по логу приложения или эндпоинту, который печатает путь.
Что хотят услышать: правило замены префикса, проверка через журнал приложения.
Красный флаг: «слэш ни на что не влияет».
10. [junior] [на скорость] Сайт отдаёт страницу «Welcome to nginx!» вместо приложения. Причины?
Ответ
Запрос попал на сервер по умолчанию. Причины: не подключён конфиг сайта (нет симлинка в sites-enabled), не совпало server_name с заголовком Host, не выключен default, либо конфиг не перечитан после правки (reload). Проверяю nginx -T, ls -l /etc/nginx/sites-enabled/, заголовок Host в запросе.
Что хотят услышать: sites-enabled, выбор server по Host, nginx -T, reload.
Красный флаг: «переустановлю nginx».
11. [middle] Прод: nginx выдаёт 502, а приложение работает и отвечает на curl 127.0.0.1:8080. Что ещё проверишь?
Ответ
Читаю точный текст в error.log. Возможные причины: nginx ходит на другой адрес или порт, чем слушает приложение (сверяю upstream: в строке ошибки с ss -ltn); приложение слушает только IPv6 ::1, а nginx идёт на 127.0.0.1; upstream sent too big header (приложение прислало слишком большие заголовки); права на сокет, если общение идёт через файл-сокет. Вывод делаю из текста ошибки, а не по догадке.
Что хотят услышать: IPv4 против IPv6 на loopback, сверка upstream: с реальным портом, upstream sent too big header, вывод из текста ошибки.
Красный флаг: «если curl работает, то nginx неправ и его надо переставить».
12. [middle] Как найти самый частый источник 5xx по логу nginx за сегодня?
Ответ
awk по полю кода и пути, отбор $9 ~ /^5/, потом sort | uniq -c | sort -rn | head: например, awk '$9 ~ /^5/ {print $7}' access.log | sort | uniq -c | sort -rn | head. Для сложных случаев добавляю в log_format время upstream и идентификатор запроса.
Что хотят услышать: пайплайн из урока 1.2, отбор по коду, идея расширенного log_format.
Красный флаг: «открою лог и посмотрю глазами».
13. [junior] [на скорость] Почему приложение слушает 127.0.0.1, а nginx 0.0.0.0?
Ответ
Чтобы снаружи был виден только nginx. Приложение на 127.0.0.1:8080 недоступно с других машин (пакеты на этот адрес приходят только изнутри). Все запросы идут через nginx: он пишет логи, задаёт заголовки, позже добавит TLS. Если открыть приложение на 0.0.0.0, до него можно достучаться в обход nginx.
Что хотят услышать: loopback доступен только локально, единая точка входа, порт закрыт не файрволом, а привязкой.
Красный флаг: «открою 8080 наружу на всякий случай».
14. [middle] Как nginx выбирает location для запроса?
Ответ
Сначала ищется точное совпадение location = /path, и если оно есть, выбор на нём заканчивается. Затем выбирается самый длинный подходящий префикс. Если у него модификатор ^~, regex-проверки пропускаются. Иначе nginx проверяет regex-локации (~, ~*) по порядку в конфиге, и первая подошедшая выигрывает. Если ни один regex не подошёл, используется найденный префикс. Поэтому порядок префиксов не важен, а порядок regex важен. Проверяю результат так: nginx -t и запрос curl -I к нужному пути.
Что хотят услышать: точное =, самый длинный префикс, ^~, regex по порядку, итог проверяю запросом.
Красный флаг: «Выбирается первая локация сверху».
15. [middle] Как настроить балансировку между несколькими бэкендами в nginx?
Ответ
Описываю группу upstream с серверами и указываю её в proxy_pass http://имя_группы;. По умолчанию запросы идут по кругу (round-robin), можно задать weight, а также least_conn или ip_hash (один клиент на одном бэкенде). Пассивная проверка здоровья встроена: после max_fails ошибок за fail_timeout сервер исключается на это время. Активные проверки есть в nginx Plus или во внешних инструментах. При сбое бэкенда nginx может повторить запрос на другой (proxy_next_upstream), поэтому повтор небезопасных запросов нужно настраивать осознанно.
Что хотят услышать: блок upstream, round-robin, weight/least_conn/ip_hash, пассивные проверки max_fails, proxy_next_upstream и идемпотентность.
Красный флаг: «Балансировка в nginx - только с платной версией».
Проверено на версиях
Практика заданий 1-5 и сценарии break.sh прогнаны в Docker-контейнере с Ubuntu 24.04.5 LTS (systemd 255) на Mac; выводы в уроке взяты из этого прогона.
- nginx 1.24.0 (пакет
1.24.0-2ubuntu7.18), curl 8.5.0, Python 3.12.3, приложениеversions/v3.py. - В контейнере nginx после
apt installне запускался сам (там запрещён автозапуск служб), поэтому я запускал его командойsystemctl start. На обычной ВМ он стартует при установке. - Ubuntu 26.04.1 LTS: только
apt install nginx,nginx -v(1.28.3) и проверкаnginx -tс ошибкой в конфиге (текст ошибки тот же), curl 8.18.0. Задания целиком на 26.04 не прогонялись. - Команда
git commitиз задания 5 не прогонялась (репозитория~/notesна стенде нет, git подключается в уроке 3.1). - Настоящее число worker, PID, адрес сетевой карты и время у тебя будут другими.
Итог урока: ты умеешь
- объяснить, что такое обратный прокси и почему соединений два
- установить nginx и включить сайт через
sites-availableиsites-enabled - настроить
proxy_passс заголовкамиHost,X-Real-IP,X-Forwarded-For,X-Forwarded-Proto - проверять конфиг через
nginx -tи применять черезreloadбез разрыва соединений - предсказать, какой
locationобработает запрос - отличить 502 от 504 по времени ответа и найти причину в
error.log - читать
access.logи считать коды ответов - закрыть порт приложения привязкой к 127.0.0.1 и оставить наружу только 80
Дальше: Урок 2.6: TLS и HTTPS
Проверь себя
Короткий тест по уроку: 5 вопросов из банка в 30. Засчитывается только полностью правильный ответ, порог 60%. Каждая новая попытка даёт другие вопросы, пока банк не закончится. Ответы видны после проверки.
Тест работает с включённым JavaScript.