gitaspen docs

Сетевой контур: от домена до микросервиса

Как трафик доходит от браузера до кода микросервиса и почему на пути стоит несколько 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, healthTLS, бизнес-логика
nginx микросервисавход в микросервис со стороны частной сетимаршрутизация чужих сервисов
backendпредметная логикасеть и публикация

Разделение позволяет менять слои по отдельности: добавление микросервиса меняет конфигурацию шлюза, но не трогает домен и сертификат; смена домена не задевает приложение.


Что публикуется наружу

Единственная точка входа снаружи — порты 80 и 443 на сервере шлюза. Всё остальное недоступно из интернета.

КомпонентПубликацияПочему так
nginx хоста80, 443 на всех интерфейсахэто и есть вход
шлюз приложения127.0.0.1:8000:80доступен только nginx хоста
nginx микросервисабез публикации, адрес в частной сетидоступен только шлюзу
backendexpose: 8000доступен только своему nginx
база данных, очередьбез публикациидоступны только своим сервисам

Проверка со стороны (с другой машины) — открыт должен быть только 443 (и 80 под редирект):

bash
curl -m 5 -I https://example.com          # ожидается ответ приложения
curl -m 5 -I http://example.com:8000      # ожидается таймаут или отказ соединения

Ответ на втором запросе означает, что шлюз опубликован на всех интерфейсах и доступен в обход TLS.


Частная сеть между серверами

Микросервисы на разных серверах соединяются частной сетью, а не через публичные адреса. Так адреса микросервисов не существуют в интернете, и правило «наружу торчит только 443» продолжает выполняться при любом числе серверов.

В Compose это выглядит как отдельный сервис-клиент частной сети, к сетевому пространству которого подключается nginx микросервиса:

yaml
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_nginxnetwork_mode: "service:vpn_client", портов на хосте нет
локальный хост (шлюз на этом же сервере)host_nginxports: ["127.0.0.1:8000:8000"]

Нужен один вход — работает один nginx; нужны оба — работают оба, каждый на своей сети. Сам сервис при этом не меняется: он не знает, откуда пришёл запрос, и его настройка не зависит от размещения.

Зачем так. Цель — чтобы сам сервис никогда не смотрел наружу. Он слушает только внутри контейнерной сети (expose), и любое обращение к нему проходит через nginx. Отсюда следует:

  • нет прямого доступа. Сервис нельзя дёрнуть в обход — снаружи нет ни порта, ни адреса, по которому он отвечает;
  • нельзя обойти проверки. Авторизация, ограничение частоты запросов, CORS, предельный размер тела живут на входе. Опубликованный порт сервиса означал бы путь мимо всего этого;
  • внутреннее не утекает. Схема API, отладочные и служебные ручки, метрики видны только тем входам, которым их открыли, а не всему интернету;
  • вход можно закрыть, не трогая сервис. Отключение или ограничение доступа — это правка nginx, а не перезапуск и переконфигурация приложения.

Из этого же следует, почему запрещена публикация порта самого сервиса: она создаёт вход, у которого нет своего nginx, — то есть путь внутрь без единой проверки.

yaml
  # вход со стороны частной сети — когда шлюз на другом сервере
  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 микросервиса.

Шлюз обращается к микросервисам по этим адресам, а сами адреса задаются переменными окружения:

nginx
upstream core_service {
    server ${CORE_SERVICE_ADDRESS} max_fails=3 fail_timeout=30s;
    keepalive 32;
}

Хранение адресов в переменных, а не в конфигурации, позволяет переносить микросервис на другой сервер, меняя только окружение шлюза.


Как проверять по слоям

Когда «сайт не работает», проверка идёт снизу вверх — от кода к домену. Первый слой, который не отвечает, и есть место отказа.

bash
# 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, микросервис не в списке, ошибка конфигурации
4nginx хоста, 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 на входе, затем выкат новых версий
Инструкция не помогла?

Откройте исходник документа по ссылке «Предложить правку» — там же видно, что и когда в нём менялось.