Ваш сервіс у Compose упирається в тайм-аут під час звернення до API, який браузер на тій самій машині відкриває без жодних скарг. Ця різниця і є підказкою: робочий запит виходить із хоста з тунелем VPN, а запит, що зависає, виходить із контейнера з власним мережевим стеком. Ніщо всередині контейнера не знає маршрутів тунелю, тому приватна адреса залишається недосяжною, хоч як би було записано ім’я. Рішення — навмисний стрибок через хост, а не хитріша таблиця маршрутизації.

Почніть із того, чому два шляхи різняться, а потім вирішіть, де має жити тунель.

Чому хост працює, а контейнер ні

Підняття тунелю дає хосту інтерфейс, набір маршрутів і зазвичай запис резолвера від VPN. Контейнер нічого з цього не успадковує: він стартує з одним інтерфейсом на мосту рушія й маршрутом за замовчуванням, що вказує на цей міст. Маршрути, встановлені на хості, невидимі для всього, що працює всередині цього простору імен.

Тому симптоми виглядають неузгодженими, а не просто зламаними. Хост розв’язує приватне ім’я й підключається, бо там є і ім’я, і маршрут. Контейнер надсилає свій пакет на приватну адресу, до якої ніщо на шляху через міст не дістане, і запит завершується тайм-аутом. Додавання імені до файлу hosts контейнера нічого не змінює, бо сама адреса недосяжна звідти, де стоїть контейнер.

Дістаньтеся хоста зсередини контейнера

Процес у контейнері, який набирає localhost, набирає сам контейнер. Docker Desktop публікує host.docker.internal як ім’я, що вказує назад на машину, де працює рушій; рушії Linux лишають його невизначеним, доки сервіс не попросить, і саме це робить прив’язка нижче. З нею застосунок націлюється на ім’я та порт замість приватної адреси.

services:
  app:
    extra_hosts:
      - "host.docker.internal:host-gateway"
    environment:
      INTERNAL_API_HOST: "host.docker.internal:8666"

Тунель залишається на боці хоста цього стрибка, а порт у тій змінній — той, на якому слухатиме проксі. Контейнер, що втратив тунель, тепер падає на одному відомому стрибку, а не намагається йти маршрутом, якого ніколи не мав.

Виберіть, де живе тунель

Для сервісу Compose, якому потрібен приватний API, можливі три форми, і вони різняться тим, який процес володіє тунелем. Клієнт VPN може працювати всередині образу контейнера, що вимагає відкрити пристрій тунелю й надати пов’язану з ним здатність адміністрування мережі. Застосунок може ділити мережевий простір імен окремого контейнера тунелю, тоді обидва процеси бачать один стек і одну таблицю маршрутизації. Або тунель може залишитися на хості, з вузьким переправником із хоста в контейнер.

Перші дві тягнуть у проєкт файли профілів, поведінку перепідключення й політику резолвера та роблять кожен сервіс у файлі залежним від здоров’я тунелю. Для одного виклику API це занадто багато зв’язності. Для стеку розробки, що читає єдину внутрішню кінцеву точку, переправник на боці хоста — найменша зміна, яка все ж лишає облікові дані там, де ними керує команда платформи.

Опублікуйте один вузький шлях на хості

Почніть із того, що успадковує переправник: запустіть його під обліковим записом, якому належить тунель, і його вихідні сокети підхоплять маршрути хоста й резолвер хоста. Один блок location у nginx покриває один шлях.

server {
    listen 8666;
    location /search/ {
        proxy_pass http://10.20.30.40:8090/;
        proxy_set_header Host internal-api.example;
        proxy_read_timeout 60s;
    }
}

Вважайте адресу й порт заготовками й підставте ту пару, на якій API справді слухає. Кожен наступний upstream заслуговує власного блоку location, і кожен блок має пропускати лише ті шляхи, які викликає цей сервіс, щоб проксі не став загальними дверима в приватну мережу. Прив’яжіть слухача до адреси моста для клієнтів, що приходять через host-gateway, до loopback лише коли клієнт ділить мережевий простір імен хоста, і ніколи до 0.0.0.0. Виберіть і порт, який ще вільний: зайнятий номер лишає проксі таким, що не слухає, а контейнер далі впирається в тайм-аут, і це не відрізнити від мережевої несправності, з якої ви почали.

Порти, імена й рішення щодо TLS

Розв’язання імен ділиться так само, як маршрутизація. Публічні запити контейнера йдуть через резолвер рушія, тому приватне ім’я, яке розв’язується на хості, не розв’яжеться всередині контейнера. Проксі — той компонент, що розв’язує приватне ім’я, бо успадковує резолвер хоста.

TLS — рішення, яке відкладають, а потім воно пояснює наступну помилку. Якщо проксі передає байти недоторканими, контейнер бачить власний сертифікат API й потребує внутрішнього CA у своєму сховищі довіри. Якщо проксі завершує TLS, ця довіра переходить на хост, і застосунок починає будувати посилання з отримуваного заголовка host. Виберіть одну схему й застосовуйте її до кожного ресурсу, бо переадресація, видана іншою, вказує на ім’я хоста, яке контейнер не може розв’язати.

Перевірте зсередини сервісу

Надсилайте запит із контейнера, а не з хоста: звичайний curl на шлях проксі, потім власна перевірка здоров’я застосунку. Коли проксі відповідає, а застосунок усе одно падає, порівняйте три речі: базовий URL, який він будує, будь-яку змінну проксі в його середовищі та CA, якому він довіряє. Усе це є в середовищі й журналах контейнера, і виправлення будь-чого з цього не чіпає VPN.

Запишіть вибрану схему в README проєкту, поки вона свіжа. Інакше наступна людина, що натрапить на цей тайм-аут, проведе той самий день, знову відкриваючи, що тунель живе на хості.

Схожі статті