Упаковка веб-приложения в Docker - довольно простая задача, если разобраться с базовыми понятиями работы контейнеров. Готовим контейнер для бэкенда, для базы данных, для фронтенд-приложения - и вуаля, приложение полноценно функционирует. В большинстве случае стандартная настройка сети и конфигурация в Docker покрывает все нужды разработчиков.

Но в данной конкретной ситуации, несмотря на кажущуюся простоту структуры проекта, этой базовой конфигурации оказалось мало.

Кейс

В работе был очень простой проект, состоящий всего из трех компонентов:

  1. SPA приложение на ReactJS

  2. Бэкенд на Symfony 6

  3. База данных PostreSQL 14

Загвоздкой оказался один нюанс. Бэкенд должен был динамически создавать новый внутренний сервер, слушающий другой порт, а приложение на React должно иметь возможность обращаться к нему напрямую.

Если бы проект не был упакован в Docker, возможно, проблемы бы и не было. Веб-сервер, виртуальный хост, настроенный заранее, и любое приложение могло бы обращаться на нужный фиксированный порт, когда сам сервер был бы развернут.

Проблема

Первоначальный вариант конфигурации Docker мог бы выглядеть примерно так.

После сборки и запуска трех контейнеров у нас есть доступ как к бэкенду через localhost:8000, так и к фронтенду (стандартно на localhost:3000). Как уже было сказано, PHP контейнер создаст новый компонент. Для простоты начнем с попытки прописать в конфигурации того же контейнера фиксированный порт для этих целей. Тогда в docker-compose.yml мы сделаем примерно такие строки:

backend:
  ports:
    - 8000:80
    - 8888:8888

И с этой же попытки мы сталкиваемся с настоящей проблемой. Во время сборки и запуска контейнеров динамического компонента не существует, и он не выделен в отдельный контейнер, так что подключиться по имени контейнера средствами Docker network не получится. Как же заставить фронтенд обратиться к новому компоненту внутри контейнера “backend”?

Решение

Чтобы справиться с этой проблемой, нам нужно немного подкорректировать настройки сети между этими двумя контейнерами.

Возможности настройки предоставляют нам не только разные типы сетей (host, bridge). На самом деле, мы можем внедрить один контейнер в сеть другого контейнера прямо в docker-compose.yml. В этом случае первый контейнер станет своего рода “мастер-контейнером”, а второй - дочерним с точки зрения сетевой настройки. И поскольку контейнер backend несет в себе все эти дополнительные сложности, сделаем мастером именно его.

Для начала добавим связку хоста с внутренней сетью контейнера. Для этого в конфигурации контейнера backend добавим секцию extra_hosts и уберем проброс портов для контейнера фронтенд-приложения, там они больше не понадобятся. Вместо этого, включим проброс порта 3000 с локальной машины сразу в контейнер ‘backend’.

Наконец, включим особый сетевой режим для контейнера ‘frontend’, присоединив его в один сетевой домен с бэкендом. Измененная конфигурация будет выглядеть следующим образом.

Результат

С помощью новой конфигурации контейнер фронтенд-приложения имеет возможность обращаться к любому порту, назначенному родительскому контейнеру ‘backend’, так как теперь у них обоих общий localhost. Мы же можем обращаться к ним через localhost нашей машины, поскольку привязали их друг к другу с помощью секции extra_hosts.

Разумеется, сфера применения такого решения довольно узкая, поскольку и сам кейс довольно специфичный. Как мы и говорили в самом начале, в подавляющем большинстве случаев стандартные настройки сети Docker справляются на отлично. Здесь же представлен выход из ситуации, когда источник проблемы кроется не в самой сети, а в аспектах динамически изменяемой среды.

Комментарии (6)


  1. LabEG
    11.01.2023 17:53

    Этот конфиг используется для локальной разработки?


  1. LabEG
    11.01.2023 18:02
    +9

    Вообще в докере можно обращаться по имени сервиса, оно же будет именем хоста.


  1. mvs
    11.01.2023 20:38
    +5

    Загвоздкой оказался один нюанс. Бэкенд должен был динамически создавать новый внутренний сервер, слушающий другой порт, а приложение на React должно иметь возможность обращаться к нему напрямую.

    А что вы на самом деле пытаетесь сделать, какую проблему решить?
    Перебор портов - не всегда хорошая идея, т.к. они могут быть недоступны, заблокированы "посередине" и т.д. Возможно, привязка к именам (хостам) на одном порту - более подходящее решение?


  1. shadowalone
    12.01.2023 02:04
    +1

    Совершенно непонятно зачем такое городить то? Внутри между собой контейнеры в одной сети могут обращаться друг к другу по имени + на любой порт.


  1. kozlyuk
    12.01.2023 03:44
    +1

    network_mode: service:donor нужен, если donor делает с сетью что-нибудь особенное, например, настраивает маршруты или поднимает VPN. Что приятно, donor'у для этого обычно нужно CAP_NET_ADMIN, а остальные контейнеры могут пользоваться его плодами, не повышая привилегий. В общем network namespace также доступны анонимные UNIX domain sockets, а значит и SCM_RIGHTS.


  1. vovababaev
    13.01.2023 17:32

    Интересное решение