mysurik.ru

Nginx reverse proxy для self-hosted сервисов

Когда на домашнем сервере набирается с десяток сервисов — Portainer там, Grafana, ntfy, может, пара сайтов — ты быстро упираешься в одну проблему: порты. Всё висит на разных портах, запомнить их невозможно, а ходить через IP:порт как-то дико. Я долго терпел, пока в один момент не понял — пора ставить reverse proxy.

Nginx я до этого использовал только как веб-сервер для статики. Обычная связка: серверный блок, root, index.html — и погнали. Но reverse proxy — это совсем другая история. Тут nginx выступает прослойкой: ты стучишься на 80-й или 443-й порт, а он уже сам решает, кому из твоих контейнеров отдать запрос. Идея простая, но когда я первый раз настраивал — наделал кучу ошибок.

Первое, что я сделал — повесил всё на один поддомен через разные location-блоки. Думал, умно будет: serv.example.com/grafana, serv.example.com/portainer и так далее. На бумаге выглядело красиво. На деле Grafana упорно отдавала 404, потому что не умеет работать из подпапки без дополнительного костыля с root_url. Portainer тоже ругался. Я потратил вечер, гугля каждую ошибку, пока не плюнул и не завёл отдельные поддомены под каждый сервис. grafana.example.com, portainer.example.com, ntfy.example.com — и всё заработало с первого раза.

Тут важно: reverse proxy не просто проксирует запросы. Он добавляет заголовки, без которых сервисы отказываются работать. X-Forwarded-For, X-Real-IP, X-Forwarded-Proto. Особенно X-Forwarded-Proto — если его не передать, некоторые сервисы думают, что работают по HTTP, и не отдают контент через HTTPS. Я на этом потерял час. Просто запомни: этот заголовок должен быть в каждом location-блоке.

Ещё момент — WebSocket. Некоторые сервисы, вроде Code-Server или порталов реального времени, используют WebSocket. Без специального апгрейда соединения nginx просто отвалится после первого рукопожатия. Я настраивал Portainer, а он открывался, показывал интерфейс, но при любом действии — разрыв. Потом до меня дошло: нужен Upgrade и Connection заголовки. Добавил — и всё залетало.

С SSL вообще отдельная песня. Когда у тебя 10 поддоменов и на каждом Lets Encrypt — это боль. Я сначала ставил сертификаты руками через certbot на каждый поддомен. Потом нашёл выход — wildcard-сертификат. Один сертификат на *.example.com, и все поддомены закрыты. Certbot с DNS-01 challenge через Cloudflare API — это магия. Раз настроил, обновляется автоматически, и ты про него забываешь.

Систему я организовал так: есть главный конфиг nginx, в котором лежит HTTP-блок с редиректом на HTTPS. И отдельные файлы в /etc/nginx/sites-enabled/ под каждый сервис. Каждый файл — это отдельный server-блок с именем поддомена, SSL-сертификатом и location-блоком с proxy_pass на нужный контейнер. Например, для Grafana — proxy_pass http://localhost:3000. Для Portainer — https://localhost:9443. Только не забудь про resolver — если используешь Docker-имена контейнеров, nginx сам их не резолвит, придётся указать DNS в явном виде.

Был у меня забавный случай: настроил всё, перезагрузил nginx, а сервисы не открываются. Смотрю логи — connect() failed. Оказалось, я забыл, что контейнеры висят на разных сетях Docker. Один сервис был в сети proxy, другой в default — они друг друга не видели. Пришлось пересоздавать контейнеры и заводить их в одну сеть. Теперь у меня все проксируемые сервисы сидят в общей сети nginx_proxy.

Ещё момент, который я пропустил в первый раз — rate limiting. Если открыть Portainer или Grafana наружу, рано или поздно по ним начнут долбиться боты. Я заметил, что откуда-то из Китая каждые 5 секунд приходит запрос к /api. Поставил лимит 10 запросов в минуту на неавторизованные запросы — и стало тихо. Nginx умеет это делать одной директивой limit_req_zone.

В итоге моя схема сейчас выглядит так: Cloudflare → мой сервер (80/443) → nginx (reverse proxy) → контейнеры. Cloudflare даёт DDOS-защиту и кеширование статики. Nginx рулит проксированием и SSL. Контейнеры вообще не смотрят наружу, у каждого内部 IP. Я сплю спокойно, зная, что даже если один сервис взломают, до других не дотянутся.

Из полезного: я написал небольшой скрипт на Python, который генерирует конфиги для новых сервисов. Запускаю, ввожу имя поддомена и порт — он создаёт файл в sites-available, включает в sites-enabled и делает certbot. Минутное дело. До этого я каждый раз копировал старый конфиг и правил вручную — было много опечаток.

Что я хочу сказать тем, кто только собирается ставить reverse proxy: не бойся. Это реально упрощает жизнь. Вместо того чтобы помнить, что Grafana на :3000, а Portainer на :9443, ты просто открываешь grafana.example.com. Всё. Единый вход, единый SSL, единый доступ.

Из минусов — надо следить за версиями. Nginx обновляется, и иногда директивы меняются. Например, раньше я использовал ssl on, теперь это deprecated, надо слушать на 443 ssl. Мелочь, но если не знать — можно долго искать проблему.

В общем, reverse proxy — это та вещь, про которую думаешь «зачем оно мне, я и так живу». А когда поставишь — не представляешь, как жил без него. У меня сейчас 12 сервисов за одним nginx, и я добавляю тринадцатый без страха. Просто копирую шаблон, меняю две строки — и готово.

Расскажу ещё деталей, которые я вынес за год работы с reverse proxy. В первой части я ужал кучу мелочей, а они как раз и определяют, будет ли настройка гладкой.

Начну с организации конфигов. У меня в /etc/nginx/sites-enabled лежит отдельный файл на каждый сервис, и это спасло мне жизнь не раз. Когда что-то ломается, ты отключаешь один файл, а не трогаешь весь nginx. Плюс я вынес повторяющиеся куски в snippets — общий блок SSL, общие заголовки безопасности, общий лимит запросов. Подключение snippet — это одна строка include, а не десять дублирующихся директив. Когда у тебя двенадцать конфигов, дублирование — это рассадник ошибок, потому что правку надо вносить в двенадцать мест.

Про отладку. Самое первое, что я делаю, если сервис не открывается — смотрю логи nginx. Но не ошибки в них главное. Главное — понять, доходит ли запрос вообще до nginx и что он отвечает. Помогают две команды:

curl -v https://grafana.example.com/
nginx -t

Вторая проверяет синтаксис конфига перед перезагрузкой. Я когда-то посадил весь сайт, потому что перезагрузил nginx с кривым конфигом. Теперь правило железное: сначала nginx -t, потом systemctl reload nginx. Если конфиг битый — nginx -t скажет, какая строка кривая, и reload не произойдёт, сайт не ляжет.

Ещё про proxy_pass есть тонкость, о которой многие спотыкаются. Если в конце адреса стоит слэш — nginx заменяет часть пути; если слэша нет — подставляет целиком. Например, proxy_pass http://localhost:3000; и proxy_pass http://localhost:3000/; ведут себя по-разному с путями. Из-за этого нюанса я однажды полчаса ловил 404 на подпапке. Проверяй слэш — это частая ошибка.

Теперь про размер клиентских тел. Если сервис принимает загрузку файлов — скажем, Nextcloud или какой-нибудь файловый менеджер — nginx по умолчанию ограничивает размер тела запроса одним мегабайтом. Мне пришлось поднять client_max_body_size до 100M, иначе загрузка картинок и архивов молча падала с 413. Смотреть логи без этого — не поймёшь, в чём дело.

Безопасность на уровне nginx. Я добавил в общий конфиг несколько вещей, которые закрывают кучу дыр. Заголовки X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Content-Security-Policy — настраиваются один раз и работают на всех сервисах. Ещё блокирую доступ к скрытым файлам (.git, .env) и лимитирую запросы к админкам. Это базовая гигиена, которая отсекает большинство сканеров. Не панацея, но фон шума в логах заметно падает.

Доступность наружу — отдельная история. Я не публикую все сервисы сразу. Внутренние инструменты — Portainer, мониторинг, базы — живут только в локальной сети или за VPN через Tailscale. Наружу торчит только то, что должно: сайт, почта, пара публичных сервисов. Для остального доступ через VPN — и это правильно. Reverse proxy снаружи виден только для белого списка поддоменов, всё остальное закрыто.

Ещё я поставил бэкапы конфигов nginx. Это три папки в git: sites-available, sites-enabled, snippets. Любая правка — коммит. Когда я что-то сломаю и не могу понять, что было раньше, git diff показывает всё. Конфиги nginx — это тоже код, и относиться к ним надо как к коду: версионировать, комментировать, не трогать руками без необходимости.

Что я посоветую новичку. Начни с одного поддомена и одного сервиса, доведи до конца: HTTPS, заголовки, редирект. Пойми, как работают location и proxy_pass. Потом добавь второй сервис. К четвёртому-пятому процесс станет рутиной, и ты начнёшь думать о сниппетах и автоматизации. Главное — не пытаться проксировать сразу всё: это путь к хаосу в конфигах и бессонным ночам за логами.

Итог. Reverse proxy — это тот слой, который превращает кучу контейнеров на разных портах в аккуратный набор поддоменов с единым SSL и безопасностью. Один раз настроив его правильно — со сниппетами, версионированием и проверкой конфигов — ты получаешь систему, которая требует почти ноль внимания. Я свой настроил полгода назад и с тех пор только добавляю новые сервисы по шаблону. Именно так это и должно работать.

Не могу не упомянуть про DNS и TTL. Когда я менял A-записи на поддомены, первое время половина мира видела старые адреса. Научился: перед тем как менять записи, снижаю TTL на минимум, жду пару часов, меняю, а после проверки возвращаю нормальный TTL. Иначе при откате придётся ждать сутки, пока DNS обновится. Мелочь, но экономит нервы.

И ещё один совет — про мониторинг самого nginx. Я повесил на него сбор метрик: активные соединения, количество запросов, время ответа. Смотрю через Prometheus и Grafana. Когда что-то начинает медленно работать — сразу видно по графику, что именно: DNS, диск или сеть. Для меня это стало дополнительной страховкой поверх логирования.

Вот такой у меня опыт. Начни аккуратно, с одного сервиса, и ты быстро поймёшь, насколько reverse proxy упрощает жизнь. Главное — не игнорировать сниппеты, версионирование и проверку конфигов перед reload. Тогда всё будет работать долго и стабильно.

Ваш комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *