devops-курс Все курсы

✻ Урок 2.5 · Тема 2: Сеть, HTTP, DNS, nginx и TLS

nginx как reverse proxy для «Заметок»

⏱ 4 ч

Зачем это нужно

«Заметки» слушают 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.

Что нужно знать

Если у тебя 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 пишет сам.

В уроке разбираются по порядку все стрелки и блоки этой схемы:

  1. что такое обратный прокси и почему соединений два;
  2. как устроены процессы nginx и как менять его настройки, не роняя сайт;
  3. как выглядит конфиг и где его файлы;
  4. как nginx выбирает блок server и блок location для запроса;
  5. как работает proxy_pass и зачем заголовки X-Forwarded-*;
  6. откуда берутся коды 502 и 504 и как читать логи.

Теория

Обратный прокси: зачем нужен посредник и почему соединений два

Программа-приложение решает одну задачу: показывать заметки. Всё вокруг неё (принять тысячи подключений, зашифровать трафик, выдержать медленных клиентов, ответить ошибкой, когда приложение упало) она делает плохо или вообще не умеет. Простой Python-сервер из app.py держит каждое подключение отдельным потоком (поток это отдельная «рука» программы: одна рука обслуживает одного клиента, и число рук ограничено), и если тысяча клиентов по слабой мобильной сети начнут медленно читать ответ, потоки закончатся. Без посредника разработчик обязан встроить всё это в каждое приложение, а при каждой замене приложения менять и то, что видят клиенты.

Слово «прокси». Прокси (proxy, «посредник») это программа, которая принимает запрос от имени кого-то другого и передаёт его дальше. Бывают два вида:

  • прямой прокси стоит на стороне клиента: ты за ним прячешься, чтобы сайт не видел твой адрес (например, корпоративный прокси, через который сотрудники выходят в интернет);
  • обратный (reverse) стоит на стороне сервера: клиенты обращаются к нему, а он выбирает, какому приложению передать запрос. Клиент даже не знает, что приложение там есть. Наш случай.

Вспомни администратора гостиницы из начала урока. Клиент говорит с администратором и получает ответ от него, а не от повара на кухне.

Теперь по шагам. Слово upstream (буквально «выше по течению») в nginx означает «то приложение, куда передаём запросы». У нас это «Заметки» на 127.0.0.1:8080. Когда клиент открывает http://notes.lab/:

  1. Клиент по DNS (урок 2.3) узнаёт адрес notes.lab и открывает TCP-соединение (урок 2.2) на порт 80. Порт 80 это стандартный порт HTTP: клиент подставляет его сам, если в адресе порта нет.
  2. Клиент отправляет HTTP-запрос: GET / HTTP/1.1, заголовок Host: notes.lab.
  3. nginx читает запрос целиком, решает, что с ним делать (как, разберём ниже), и открывает второе TCP-соединение, уже к 127.0.0.1:8080.
  4. nginx отправляет приложению запрос (свой, заново собранный).
  5. Приложение отвечает 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.

  1. По адресу и порту (listen) находятся все server, которые слушают этот порт.
  2. Среди них ищется тот, у которого имя в server_name совпадает с заголовком Host запроса.
  3. Если совпадения нет, запрос отдаётся серверу по умолчанию для этого порта. Им становится либо тот, где стоит пометка 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 проверяет так:

  1. Ищет точное совпадение (=). Нашёл: берёт его и больше не смотрит.
  2. Иначе выбирает самый длинный префикс среди всех location без регулярок (и с ^~). Запоминает его. Если это ^~, то на этом всё: регулярки не проверяются.
  3. Иначе идёт по регуляркам сверху вниз в порядке записи в файле. Первая подошедшая побеждает.
  4. Если ни одна регулярка не подошла, берётся запомненный префикс.

Так что регулярка побеждает обычный префикс, даже более длинный. Это неочевидно, разберём на примере.

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 она включена по умолчанию:

  1. Приложение отвечает nginx быстро, по соединению между ними (второе соединение из первого раздела).
  2. nginx складывает ответ в свою память (при большом размере во временный файл на диске).
  3. Приложение освобождается сразу и закрывает соединение (вот почему nginx просит Connection: close).
  4. 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 может выполнять перед приложением, входят:

  1. Шифрование (TLS). Nginx принимает зашифрованное соединение на порту 443, расшифровывает и передаёт приложению обычный HTTP. Приложению не нужно ничего знать о сертификатах (урок 2.6). Именно поэтому нужен заголовок X-Forwarded-Proto.
  2. Раздача статических файлов. Картинки, стили, скрипты не меняются, и nginx отдаёт их сам прямо с диска (директива root или alias внутри location), приложение к этому не привлекается.
  3. Балансировка. Когда приложений несколько, в блоке upstream { server ...; server ...; } перечисляют все, и nginx распределяет между ними запросы. В курсе она появится позже вместе с Kubernetes.
  4. Ограничение нагрузки. 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/....

Шаги:

  1. Убедись, что порт 80 свободен, и поставь nginx из репозитория Ubuntu. Разбор: ss -ltn 'sport = :80' показывает слушающие TCP-сокеты (-l слушающие, -t TCP, -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.

  2. Посмотри процессы и порт. Разбор: ps показывает процессы; -C nginx выбирает те, у которых команда называется nginx; -o pid,user,cmd задаёт колонки (номер, владелец, команда):

    ps -o pid,user,cmd -C nginx
    ss -ltn 'sport = :80'
    
  3. Проверь ответ и найди, какой конфиг его отдаёт. У 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 (исходное имя из запроса), а не подставляем адрес приложения.

Шаги:

  1. Создай файл конфига в репозитории. Команда 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).
  2. Установи конфиг, включи сайт, выключи дефолтный и проверь синтаксис. Разбор: 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
    
  3. Примени без разрыва соединений и проверь. Флаг -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 его передал как есть.

Шаги:

  1. Очисти логи, чтобы старые запросы не мешали счёту. 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
    
  2. Прочитай последние строки и посчитай коды (приёмы из урока 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
    
  3. Посчитай долю ошибок 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). Разница во времени ответа тоже диагностический признак.

Шаги:

    1. Останови приложение, сделай запрос и прочитай 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
    
    1. Замерь время, запрос будет висеть 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
    
  1. Проверь, что при sec=5 всё в порядке (5 секунд меньше 30):

    curl -s 'http://notes.lab/slow?sec=5'
    
  2. Подожди ещё секунд десять (пока пройдут 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.

Шаги:

  1. Убедись, что в /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'
    
  2. Убедись, что файл в репозитории и установленный конфиг совпадают. diff A B печатает различия и ничего, если файлы равны, а && echo same печатает same только при успехе:

    diff ~/notes/deploy/nginx/notes.conf /etc/nginx/sites-available/notes && echo same
    
  3. Проверь весь путь, включая 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
    
  4. Проверь снаружи: возьми адрес сетевой карты и обратись к нему. Разбор первой строки тот же, что в уроке 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"
    
  5. Убедись, что nginx стартует при загрузке:

    systemctl is-enabled nginx
    
  6. Закоммить конфиг:

    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 версии для своего симптома. Для этих симптомов подходят:

  1. В конфиге nginx ошибка синтаксиса или он не применён.
  2. Приложение остановлено, слушает другой порт или не тот адрес.
  3. Upstream принимает соединение, но не отвечает (таймаут).
  4. Запрос попал не в тот 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.

тема 2 урок 2.5 4 ч курс 0/0 ← → уроки