Этот материал продолжает цикл о сборке Docker-образов для приложений на различных языках программирования с помощью утилиты dapp. Предыдущая статья была о сборке приложений на Java — теперь же поговорим о приложениях на JavaScript. Для начала это будет frontend-приложение, а в следующей части планируется рассказать о сборке backend'а и запуске всего в Kubernetes.
В качестве иллюстрации будут использованы приложения nodejs-pool и poolui. Да-да, подготовим к запуску в Kubernetes свой майнинг-пул с блокчейном и выплатами!
Пул для майнинга — это приложение для координации программ-майнеров. Пул раздаёт майнерам задания и собирает ответы от них. Если общими усилиями удалось найти блок, который сеть признаёт валидным, то вознаграждение за этот блок делится между участниками пула по той или иной стратегии. Nodejs-pool — это серверная часть пула, с которой общаются программы-майнеры. Poolui — frontend-приложение, с помощью которого участники взаимодействуют с пулом: регистрируются, видят общую и свою статистику по блокам, майнерам, выплатам.
Сборка приложения poolui «как есть»
Первое описание сборки может быть повторением готового Dockerfile (как в статье про Java), но если такого нет, то достаточно начать с запуска приложения в контейнере по рекомендациям разработчиков. В нашем случае в README написано, что для сборки и запуска достаточно выполнить команду npm start
— вот с этого и начнём.
Возьмём официальный образ node, в котором уже есть npm, — например, версию 9.11-alpine. В образ нужно добавить исходный код приложения, а контейнер будет запущен с командой npm start
с помощью dapp dimg run
. Исходники обычно добавляются в какую-нибудь директорию — например, /app, — поэтому нужна директива docker.WORKDIR
, чтобы команда запустилась в нужной директории, а не в корне образа. Получился такой простейший dappfile.yaml
:
dimg: poolui
from: node:9.11-alpine
git:
- add: /
to: /app
docker:
WORKDIR: /app
Команда сборки:
dapp dimg build
(Подробнее о некоторых удобствах при работе с этой командой сборки см. в статье от коллеги.)
Команда запуска приложения:
dapp dimg run poolui -p 8080:8080 -ti --rm -- npm start
После такого старта приложение в браузере будет выглядеть примерно так:
Улучшенный dappfile.yaml
По виду страницы можно понять, что одной команды npm start
не хватает для полноценного запуска приложения в виде контейнера.
Подобный запуск «как есть» работает не для всех проектов, но его можно использовать, чтобы «пронаблюдать» за стартом незнакомого приложения в контейнере. Наличие в проекте Dockerfile
упрощает работу по созданию dappfile.yaml
, т.к. сразу видно все команды для создания образа.
Чтобы создать dappfile.yaml
для проекта без Dockerfile
, нужно определить из описания сборки приложения, какие команды запускать. Инженеры нашей компании проводят такую работу либо на основе своих знаний утилит в разных языках (npm, gulp, maven, composer, и т.д.), либо в некоторых случаях совместно с разработчиками заказчика изменяют файлы описания (package.json
, gulpfile.js
, pom.xml
, composer.json
), чтобы сборка приложения описывала все нужные зависимости.
Обычно всё начинается со стадии beforeInstall
, где описывается установка системных пакетов, например, если каких-то инструментов не хватает в выбранном базовом образе. Затем добавляется стадия install
, где уже доступны исходные тексты приложения и можно запускать инструменты сборки. После успешной сборки образа с двумя стадиями можно пойти дальше и какие-то команды выделить в другие стадии (beforeSetup
, setup
), а также описать наборы директорий и файлов, изменения в которых приведут к пересборке.
Подобный запуск «как есть» для проектов, в составе которых нет Dockerfile
, обычно не применим в production-окружении. Для выката приложения в production нужно провести работу по определению команд для каждой стадии сборки образа и по определению файлов-зависимостей для этих стадий.
Рассматриваемый проект имеет три файла описания:
-
package.json
— описание зависимостей и действий для npm; -
bower.json
— описание зависимостей приложения; -
gulpfile.js
— описание задач.
Из package.json
видно, что для сборки понадобятся gulp и bower — эти утилиты можно установить на стадии beforeInstall
, т.к. их версия не будет часто изменяться. На этой же стадии добавится git, т.к. он нужен для скачивания зависимостей. В этом же файле видно команды, которых не хватило: npm install
, bower install
. Эти команды уйдут на стадию install
, где происходит скачивание зависимостей. Отличие небольшое — для простоты bower install
выполняется с ключом --allow-root
. Пересборка стадии install
зависит от изменений в файлах описаниях package.json
и bower.json
.
npm start
запускает команду gulp
, которая выполнит задачу по умолчанию из gulpfile.js
. Эта задача последовательно запускает три других: build
, connect
, watch
. Т.е. задачи сборки и запуска объединены. Это удобно для быстрого старта, но для сборки образа придётся на стадии setup
явно вызвать gulp build
и установить пересборку setup
в зависимости от изменений в директории app
и файла gulpfile.js
.
Для запуска приложения теперь используется не npm start
, а gulp connect
. Запускать gulp watch
не нужно, т.к. это команда для упрощения разработки.
Итоговый dappfile.yaml
выглядит так:
dimg: poolui
from: node:9.11-alpine
git:
- add: /
to: /app
stageDependencies:
install:
- package.json
- bower.json
beforeSetup:
- app
- gulpfile.js
shell:
beforeInstall:
- apk update
- apk add git
- npm install --global bower
- npm install --global gulp
install:
- cd /app
- npm install
- bower install --allow-root
beforeSetup:
- cd /app
- gulp build
docker:
WORKDIR: "/app"
CMD: ["gulp", "connect"]
Лог сборки приложения можно увидеть в этом asciicast'е:
Для проверки, если запустить сборку повторно, то всё соберётся из кэша:
Теперь можно запустить приложение командой dapp dimg run poolui -p 8080:8080 -ti --rm
:
В выводе запуска заметна такая строка: Server started http://localhost:8080
.
Это значит, что подключиться к серверу из браузера на хост-машине не получится. Нужно поправить gulpfile.js
, чтобы сервер слушал на адресе 0.0.0.0. Изменения нужно закоммитить и запустить dapp dimg build
, чтобы собрать новый образ. Т.к. изменится gulpfile.js
, то должна пересобраться только стадия beforeSetup
. Результат можно увидеть в этом asciicast'е:
Повторный запуск с командой dapp dimg run poolui -p 8080:8080 -ti --rm
:
Если подключиться к приложению браузером (зайти на http://localhost:8080
), то можно увидеть подобную страницу:
Готово! Фронтенд пула запущен и выглядит как нужно. Однако gulp connect
— это модуль, предназначенный для разработки, а для production хотелось бы запаковать приложение «правильно». Таким вариантом может стать образ с nginx, куда будет скопировано содержимое директории /app/build
. В dapp есть артефакты и промежуточные или инструментальные образы — эта возможность и будет использована далее.
Упаковка в образ с nginx
Приложение собирается в директорию /app/build
и достаточно превратить описанный dimg в artifact. Стоит отметить, что artifact не может содержать директивы Docker, поэтому их нужно удалить, а в остальном отличий от dimg нет. Теперь нужно добавить второй dimg на основе, например, nginx:stable-ansible. Готовый образ nginx настроен на отдачу статических файлов из /usr/share/nginx/html
— в эту директорию нужно импортировать /app/build
. Готовый dappfile.yml
выглядит так:
artifact: poolui-builder
from: node:9.11-alpine
git:
- add: /
to: /app
stageDependencies:
install:
- package.json
- bower.json
beforeSetup:
- app
- gulpfile.js
shell:
beforeInstall:
- apk update
- apk add git
- npm install --global bower
- npm install --global gulp
install:
- cd /app
- npm install
- bower install --allow-root
beforeSetup:
- cd /app
- gulp build
---
dimg: poolui
from: nginx:stable-ansible
import:
- artifact: poolui-builder
add: /app/build
to: /usr/share/nginx/html
after: install
Команда сборки не меняется:
dapp dimg build
А вот команду запуска надо поменять — nginx слушает на 80-м порту.
dapp dimg run poolui -p 8080:80 -ti --rm
При обращении к localhost:8080 результат такой же, как и с запуском gulp connect
: страница показывается полностью.
В логе сборки можно подсмотреть id стадий и оценить размер промежуточного и итогового образов.
# Итоговый образ
dimgstage-poolui 0e27eaebff1f23312d18f83370ee30000a97139311fa141b86aa3f34b15e544d 6a1a7fc1aa98 8 minutes ago 25.1MB
# Промежуточный образ poolui-builder
dimgstage-poolui f000dbe70d25a5abb998997e6c0530db52f228fee4cdd0390046657bdb3a6d32 8f300d169e5e 17 minutes ago 241MB
Теперь frontend-часть точно готова! Далее нужно собрать и запустить backend, что несколько сложнее и будет описано в следующей статье.
Заключение
Приложение довольно простое, но даже на таком примере можно показать, что контейнеризация приложений, изначально написанных без оглядки на Docker, требует некоего вдумчивого анализа. Зато в результате может получиться dappfile.yaml
, который будет пересобирать только нужные стадии, ускоряя инкрементальные сборки.
Специфика контейнеризации приложений на JavaScript именно в этом — в определении того, что нужно запускать на этапе сборки, т.к. инструментов много и у каждого заказчика свой сформированный разработчиками набор.
P.S.
Читайте также в нашем блоге:
- «Сборка проектов с dapp. Часть 1: Java»;
- «Сборка и дeплой приложений в Kubernetes с помощью dapp и GitLab CI»;
- «Практика с dapp. Часть 1: Сборка простых приложений»;
- «Практика с dapp. Часть 2. Деплой Docker-образов в Kubernetes с помощью Helm»;
- «Собираем Docker-образы для CI/CD быстро и удобно вместе с dapp (обзор и видео доклада)»;
- «Официально представляем dapp — DevOps-утилиту для сопровождения CI/CD».
Автор: diafour