
Этот материал продолжает цикл о сборке 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».