Докер: Как разделить один и тот же код приложения между несколькими контейнерами / сервисами (например, PHP-FPM и NGINX)
ТЛ; др
- код приложения требует шага сборки (тянуть в зависимости)
- нескольким контейнерам нужен один и тот же "встроенный" код
Q: какая хорошая стратегия / рабочий процесс для архивирования с помощью docker / docker-compose.
Долго
Мы находимся в процессе стыковки приложения PHP с несколькими компонентами (контейнерами / службами), например
- Рабочие узлы (процессы PHP поддерживаются через супервизор)
- Планировщик (управление рабочими и запуск повторяющихся задач в cron)
- PHP-FPM / Nginx (веб-интерфейс)
Сервисы определены в файле docker-compose. Во время разработки мы монтируем код приложения через том из каталога на хосте в каждом контейнере, чтобы мы видели изменения "немедленно" в каждом сервисе ( пример). Жизнь была хорошей.
Сейчас мы настраиваем среду CI/CD, основанную на Jenkins, которая должна создавать (+ тестировать) контейнеры и затем передавать их в реестр. Поскольку "монтировать с хоста" больше невозможно, мне интересно, как лучше всего поместить код приложения в каждый контейнер.
Две вещи в нашей установке делают это imho особенно сложным:
- у нас есть несколько контейнеров, которым нужен один и тот же код приложения
- "Артефакт сборки" - это не отдельный двоичный файл-контейнер (как у нас, например, с go), а "весь наш код + установленные зависимости" ==> "много файлов" (медленно...)
- есть этап сборки, который требует программного обеспечения, которое не требуется в конечном изображении
Решение для "3". обычно: Используйте многоэтапные сборки. Мы делаем это. Но: все примеры там, кажется, предполагают, что встроенный код будет использоваться только в одном другом контейнере (что в нашем случае неверно, см. 1.)
Чем мы сейчас занимаемся
- структура папок
application-code/
.docker/
builder/
Dockerfile
php-fpm/
Dockerfile
docker-compose.yml
build.sh
index.php
- ввести дополнительный контейнер "builder", который "собирает" приложение (получает "весь" код приложения в качестве контекста сборки; запускает "composer install")
# ./builder/Dockerfile
COPY ./ /codebase
RUN cd /codebase && composer install
- "Копировать" из этого компоновщика в каждый контейнер, для которого требуется код приложения, например, через
# ./php-fpm/Dockerfile
ARG APP_CODE_PATH="/var/www/current"
COPY --from=builder --chown=www-data /codebase ${APP_CODE_PATH}
- организован через docker-compose
# ./docker-compose.yml
version: '3.7'
services:
builder-ci:
image: builder
build:
# ../ contains the "raw" application code
context: ../
dockerfile: ./.docker/builder/Dockerfile
php-fpm:
build:
context: .
dockerfile: ./php-fpm/Dockerfile
args:
- APP_CODE_PATH=/var/www/current
- строить через
# build.sh
## build builder
docker-compose -f ./.docker/docker-compose.yml --project-directory ./.docker build builder
## build the rest
docker-compose -f ./.docker/docker-compose.yml --project-directory ./.docker build --parallel
профессионал
- "меньшие" изображения (в php-fpm не будет установлен композитор)
- код приложения создается только один раз, а затем копируется поверх
против
- контейнер конструктора не служит никакой другой цели, кроме как "встроенный" ==> не чувствует себя чистым
- сборка "строителя" должна быть сделана до того, как будет построен любой другой контейнер
- это означает, что у нас есть дополнительный
альтернативы
- не используйте контейнер компоновщика, но "включите" этап компоновки, например, в контейнер "Планировщик", используя многоэтапные компоновки (поэтому мы не получим в конечном итоге composer)
- get избавляется от "builder" - но теперь все другие службы зависят от "Scheduler" ==> чувствует себя еще более грязным
- использовать том, чтобы поделиться кодом
- нам не нужно "копировать" файлы в изображениях, мы можем просто "смонтировать том" ==> чувствует себя "чистым" / нет "дублирования файлов" (сначала я подумал, что это действительно хороший подход...)
- НО:
- Вы не можете заполнять тома во время сборки, поэтому вам нужно "запустить" контейнер, чтобы получить код приложения "в" контейнере ==> у нас внезапно появился не только контейнер компоновщика, но нам также нужно его "запустить" заполнить объем
- контейнеры больше не являются "самодостаточными", то есть извлечение "просто Планировщика" из реестра не будет работать - мы ДОЛЖНЫ также иметь том на месте И его должен заполнять строитель ==> оркестровка становится более сложной
- том не является эфемерным, то есть он будет содержать "старый" код приложения, пока не будет обновлен ==> это может привести к путанице и неожиданному поведению
связи
- Могут ли контейнеры иметь общую структуру? (связанный вопрос)
- https://github.com/moby/moby/issues/14080 (подключенные тома во время сборки не поддерживаются)
- https://docs.docker.com/develop/develop-images/multistage-build/ (шаблон сборки против многоступенчатой сборки)
1 ответ
Беспокойство о количестве шагов не должно быть дискуссией. Вы должны беспокоиться о том, содержит ли изображение то, что вам нужно, без какого-либо шума.
Одной из вещей, которой явно не хватает в вашем методе компоновщика, является то, что вы продвигаете кодовую базу к производству, даже если вы запускали composer только install
, Для того, чтобы запустить кодовую базу промышленного уровня, вам, по крайней мере, нужно запустить composer install --prefer-dist --no-dev -o
чтобы:
- удалить файлы, исключенные в
.gitattributes
используя--prefer-dist
(это по умолчанию, так как композитор стабилен) - исключив загрузку require-dev с
--no-dev
- путем оптимизации файлов автозагрузчика с
-o
Эта команда полностью отличается от той, которая использовалась для запуска ваших тестов.
Конвейер по умолчанию при работе с контейнером вашей кодовой базы будет:
- build: оформить заказ, установить композитор и разрешить его повторное использование в следующих шагах или создать "образ тестирования"
- test: запустить тесты и включить или отделить покрытие (одновременно)
- подготовить: повторно использовать файлы сборки и запустить установку composer с необходимыми производственными флагами
- изображение: создать изображение из предыдущего шага
Лично я считаю, что ответы на ваш вопрос весьма самоуверенны. Может быть, мой подход, используемый в работе с kubernetes, помогает.