Упаковка веб-приложения в Docker - довольно простая задача, если разобраться с базовыми понятиями работы контейнеров. Готовим контейнер для бэкенда, для базы данных, для фронтенд-приложения - и вуаля, приложение полноценно функционирует. В большинстве случае стандартная настройка сети и конфигурация в Docker покрывает все нужды разработчиков.
Но в данной конкретной ситуации, несмотря на кажущуюся простоту структуры проекта, этой базовой конфигурации оказалось мало.
Кейс
В работе был очень простой проект, состоящий всего из трех компонентов:
SPA приложение на ReactJS
Бэкенд на Symfony 6
База данных 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)
LabEG
11.01.2023 18:02+9Вообще в докере можно обращаться по имени сервиса, оно же будет именем хоста.
mvs
11.01.2023 20:38+5Загвоздкой оказался один нюанс. Бэкенд должен был динамически создавать новый внутренний сервер, слушающий другой порт, а приложение на React должно иметь возможность обращаться к нему напрямую.
А что вы на самом деле пытаетесь сделать, какую проблему решить?
Перебор портов - не всегда хорошая идея, т.к. они могут быть недоступны, заблокированы "посередине" и т.д. Возможно, привязка к именам (хостам) на одном порту - более подходящее решение?
shadowalone
12.01.2023 02:04+1Совершенно непонятно зачем такое городить то? Внутри между собой контейнеры в одной сети могут обращаться друг к другу по имени + на любой порт.
kozlyuk
12.01.2023 03:44+1network_mode: service:donor
нужен, еслиdonor
делает с сетью что-нибудь особенное, например, настраивает маршруты или поднимает VPN. Что приятно,donor
'у для этого обычно нужноCAP_NET_ADMIN
, а остальные контейнеры могут пользоваться его плодами, не повышая привилегий. В общем network namespace также доступны анонимные UNIX domain sockets, а значит иSCM_RIGHTS
.
LabEG
Этот конфиг используется для локальной разработки?