Сетевой контур: от домена до микросервиса
Как трафик доходит от браузера до кода микросервиса и почему на пути стоит несколько nginx. Схема описана для распределённой установки, где микросервисы живут на разных серверах и соединены частной сетью.
8 минутКак трафик доходит от браузера до кода микросервиса и почему на пути стоит несколько nginx. Схема описана для распределённой установки, где микросервисы живут на разных серверах и соединены частной сетью.
Документ отвечает на вопросы: что публикуется наружу, что не публикуется никогда, какой nginx за что отвечает и где искать причину, когда «сервис не отвечает».
Цепочка
браузер
│ https://example.com
▼
nginx хоста сервер шлюза, порт 443
│ TLS завершается здесь; дальше — обычный HTTP внутри машины
│ proxy_pass → 127.0.0.1:8000
▼
шлюз приложения (nginx) контейнер, порт 80, опубликован на 127.0.0.1:8000
│ маршруты, CORS, служебные ручки
│ proxy_pass → upstream по адресу частной сети
▼
частная сеть (VPN) отдельный сервер микросервиса
▼
nginx микросервиса слушает адрес частной сети
│ proxy_pass → backend:8000
▼
backend expose 8000; на хосте не публикуетсяКаждый слой решает одну задачу и не знает о задачах соседей.
| Слой | Отвечает за | Не отвечает за |
|---|---|---|
| nginx хоста | домен, сертификат, HTTP→HTTPS | маршруты приложения |
| шлюз приложения | маршруты на микросервисы, CORS, health | TLS, бизнес-логика |
| nginx микросервиса | вход в микросервис со стороны частной сети | маршрутизация чужих сервисов |
| backend | предметная логика | сеть и публикация |
Разделение позволяет менять слои по отдельности: добавление микросервиса меняет конфигурацию шлюза, но не трогает домен и сертификат; смена домена не задевает приложение.
Что публикуется наружу
Единственная точка входа снаружи — порты 80 и 443 на сервере шлюза. Всё остальное недоступно из интернета.
| Компонент | Публикация | Почему так |
|---|---|---|
| nginx хоста | 80, 443 на всех интерфейсах | это и есть вход |
| шлюз приложения | 127.0.0.1:8000:80 | доступен только nginx хоста |
| nginx микросервиса | без публикации, адрес в частной сети | доступен только шлюзу |
| backend | expose: 8000 | доступен только своему nginx |
| база данных, очередь | без публикации | доступны только своим сервисам |
Проверка со стороны (с другой машины) — открыт должен быть только 443 (и 80 под редирект):
curl -m 5 -I https://example.com # ожидается ответ приложения
curl -m 5 -I http://example.com:8000 # ожидается таймаут или отказ соединенияОтвет на втором запросе означает, что шлюз опубликован на всех интерфейсах и доступен в обход TLS.
Частная сеть между серверами
Микросервисы на разных серверах соединяются частной сетью, а не через публичные адреса. Так адреса микросервисов не существуют в интернете, и правило «наружу торчит только 443» продолжает выполняться при любом числе серверов.
В Compose это выглядит как отдельный сервис-клиент частной сети, к сетевому пространству которого подключается nginx микросервиса:
networks:
host_net: { driver: bridge }
vpn_net: { driver: bridge }
services:
vpn_client: # клиент частной сети
build: { context: ., dockerfile: vpn/Dockerfile }
networks: [vpn_net]
privileged: true
devices: ["/dev/net/tun:/dev/net/tun"]
service_nginx: # вход в микросервис со стороны частной сети
build: { context: ., dockerfile: nginx/Dockerfile }
network_mode: "service:vpn_client" # общее сетевое пространство с клиентом
depends_on: [backend]
environment:
- BACKEND_HOST=10.0.0.100
- BACKEND_PORT=8000
backend:
build: { context: ., dockerfile: source/Dockerfile }
networks: [host_net, vpn_net]
expose: ["8000"] # порт объявлен, но не опубликован
depends_on: [vpn_client]network_mode: "service:vpn_client" помещает nginx в сетевое пространство клиента частной сети:
контейнер получает её интерфейс и адрес. Поэтому nginx виден соседним серверам по адресу частной
сети, при этом на хосте не публикуется ни один порт.
На каждый вход — свой nginx
Правило: сколько у сервиса входов, столько перед ним и nginx. Вход — это сеть, из которой к сервису приходят: частная сеть между серверами и локальный хост — разные входы, и у каждого свой nginx со своей привязкой.
| Вход | Свой nginx | Как объявлен |
|---|---|---|
| частная сеть (шлюз на другом сервере) | vpn_nginx | network_mode: "service:vpn_client", портов на хосте нет |
| локальный хост (шлюз на этом же сервере) | host_nginx | ports: ["127.0.0.1:8000:8000"] |
Нужен один вход — работает один nginx; нужны оба — работают оба, каждый на своей сети. Сам сервис при этом не меняется: он не знает, откуда пришёл запрос, и его настройка не зависит от размещения.
Зачем так. Цель — чтобы сам сервис никогда не смотрел наружу. Он слушает только внутри
контейнерной сети (expose), и любое обращение к нему проходит через nginx. Отсюда следует:
- нет прямого доступа. Сервис нельзя дёрнуть в обход — снаружи нет ни порта, ни адреса, по которому он отвечает;
- нельзя обойти проверки. Авторизация, ограничение частоты запросов, CORS, предельный размер тела живут на входе. Опубликованный порт сервиса означал бы путь мимо всего этого;
- внутреннее не утекает. Схема API, отладочные и служебные ручки, метрики видны только тем входам, которым их открыли, а не всему интернету;
- вход можно закрыть, не трогая сервис. Отключение или ограничение доступа — это правка nginx, а не перезапуск и переконфигурация приложения.
Из этого же следует, почему запрещена публикация порта самого сервиса: она создаёт вход, у которого нет своего nginx, — то есть путь внутрь без единой проверки.
# вход со стороны частной сети — когда шлюз на другом сервере
vpn_nginx:
build: { context: ., dockerfile: nginx/Dockerfile }
network_mode: "service:vpn_client"
environment: [BACKEND_HOST=10.0.0.100, BACKEND_PORT=8000]
depends_on: [backend]
# вход с этого же хоста — когда шлюз рядом; включается вместо предыдущего
# host_nginx:
# build: { context: ., dockerfile: nginx/Dockerfile }
# environment: [BACKEND_HOST=10.0.1.100, BACKEND_PORT=8000]
# networks: [host_net]
# ports: ["127.0.0.1:8000:8000"]
# depends_on: [backend]Неиспользуемый вход остаётся закомментированным рядом: при переносе сервиса на другой сервер переключение сводится к смене входа, а не к переписыванию описания.
Что не меняется ни в одном случае — сам backend: у него expose, и он недоступен ни с хоста, ни
из сети. Оба входа проксируют к нему изнутри. Публикация порта самого backend сделала бы обходной
путь мимо nginx микросервиса.
Шлюз обращается к микросервисам по этим адресам, а сами адреса задаются переменными окружения:
upstream core_service {
server ${CORE_SERVICE_ADDRESS} max_fails=3 fail_timeout=30s;
keepalive 32;
}Хранение адресов в переменных, а не в конфигурации, позволяет переносить микросервис на другой сервер, меняя только окружение шлюза.
Как проверять по слоям
Когда «сайт не работает», проверка идёт снизу вверх — от кода к домену. Первый слой, который не отвечает, и есть место отказа.
# 1. backend внутри своего compose
docker compose exec service_nginx curl -sf http://10.0.0.100:8000/health && echo OK
# 2. микросервис со стороны частной сети (с сервера шлюза)
curl -sf http://10.0.0.10:8000/health && echo OK
# 3. шлюз на своём сервере
curl -sf http://127.0.0.1:8000/health && echo OK
# 4. весь контур снаружи
curl -sfI https://example.com/health && echo OK| Первый неотвечающий слой | Где искать |
|---|---|
| 1 | приложение не запустилось: docker compose logs backend |
| 2 | частная сеть: клиент не поднял туннель, нет маршрута, адрес занят другим сервером |
| 3 | шлюз: неверный upstream, микросервис не в списке, ошибка конфигурации |
| 4 | nginx хоста, DNS или сертификат — см. документ о HTTPS |
Служебная ручка /health должна быть на каждом слое: она отвечает без обращения к базе и внешним
системам, поэтому по ней проверяется именно связность, а не работоспособность всей системы.
Типичные отказы
| Признак | Причина | Что делать |
|---|---|---|
502 от домена | шлюз не отвечает на 127.0.0.1:8000 | проверить, что контейнер шлюза запущен и порт опубликован на loopback |
502 от шлюза | микросервис недоступен по адресу частной сети | проверить туннель и адрес в переменных окружения шлюза |
504 | микросервис отвечает дольше таймаута | смотреть логи микросервиса; поднимать таймаут только после выяснения причины |
сервис доступен по http://example.com:8000 | шлюз опубликован без адреса | заменить публикацию на 127.0.0.1:8000:80 |
| после переноса микросервиса всё сломалось | адрес зашит в конфигурацию шлюза | вынести адрес в переменную окружения |
| CORS-ошибка в браузере при работающем API | заголовки выставляют и шлюз, и приложение | оставить CORS только на шлюзе, из приложения убрать |
| часть маршрутов ведёт не туда | пересекающиеся location в конфигурации шлюза | проверить порядок и точность совпадений |
Место в цепочке документов
| Откуда пришли | Этот документ | Куда ведёт |
|---|---|---|
| установленный Docker | как соединены слои и что публикуется | настройка домена и TLS на входе, затем выкат новых версий |
Откройте исходник документа по ссылке «Предложить правку» — там же видно, что и когда в нём менялось.