Исчерпывающее руководство по написанию Dockerfile для веб-приложений на Node.js

Перевод статьи Praveen Durairaj: An Exhaustive Guide to Writing Dockerfiles for Node.js Web Apps. Опубликовано с разрешения автора.

TL;DR

Данный пост состоит из нескольких примеров, начиная от простого Dockerfile до многоэтапных продакшен-сборок для веб-приложений Node.js. Краткое описание того, что охватывает данное руководство:

  • Использование соответствующего базового образа (carbon для разработки, alpine для продакшена).
  • Использование nodemon для горячей перезагрузки во время разработки.
  • Оптимизация для уровней кеша Docker — размещение команд в правильном порядке, так что npm installвыполняется только при необходимости.
  • Обслуживание статических файлов (бандлов, созданных React/Vue/Angular), используя пакет serve.
  • Использование многоэтапной сборки alpine для уменьшения окончательной продакшен-сборки.
  • Советы профи: 1) Использование COPY вместо ADD 2) Обработка сигналов ядра (kernel signals) при нажатии CTRL-C при помощи флага init

Если вы сразу хотите перейти к коду, посмотрите репозиторий на GitHub.

Оглавление

  1. Простой Dockerfile и .dockerignore
  2. Горячая перезагрузка с nodemon
  3. Оптимизации
  4. Обслуживание статических файлов
  5. Одноэтапная продакшен-сборка
  6. Многоэтапная продакшен-сборка

Давайте представим простую структуру каталогов. Наше приложение будет называться node-app. В каталоге верхнего уровня есть два файла: Dockerfile и package.json. Исходный код node-приложения будет находятся в каталоге src. Для краткости предположим, что файл server.js содержит код Express-сервера, запущенного на порте 8080.

node-app
├── Dockerfile
├── package.json
└── src
└── server.js

1. Простой пример Dockerfile

Для базового образа мы использовали последнюю LST-версию node:carbon.

Во время сборки образа Docker берёт все файлы в директории приложения. Для увеличения производительности сборки Docker, исключим файлы и директории, добавив файл .dockerignore.

Как правило, ваш файл .dockerignore должен быть таким:

.git
node_modules
npm-debug

Соберём и запустим этот образ:

$ cd node-docker
$ docker build -t node-docker-dev .
$ docker run --rm -it -p 8080:8080 node-docker-dev

Приложение будет доступно по URL http://localhost:8080. Используйте Ctrl+C для завершения сервера.

Теперь предположим, что вы хотите, чтобы сборка пересобиралась при каждом изменении кода, то есть во время разработки. Тогда вы должны примонтировать файлы исходного кода в контейнер при запуске и остановки node-сервера.

$ docker run --rm -it -p 8080:8080 -v $(pwd):/app \
node-docker-dev bash
root@id:/app# node src/server.js

2. Горячая перезагрузка с Nodemon

nodemon — популярный пакет, который будет отслеживать файлы в каталоге, в котором он был запущен. Если какие-нибудь файлы будут изменены, то nodemon автоматически перезагрузит ваше приложение.

Мы соберём образ и запустим nodemon, чтобы код приложения обновлялся всякий раз, когда в директории appпроисходят изменения.

$ cd node-docker
$ docker build -t node-hot-reload-docker .
$ docker run --rm -it -p 8080:8080 -v $(pwd):/app \
node-hot-reload-docker bash
root@id:/app# nodemon src/server.js

Все изменения в папке app будут вызывать пересборку приложения, и изменения будут доступны в режиме реального времени по URL http://localhost:8080. Обратите внимание, что мы примонтировали файлы приложения в контейнер, чтобы nodemon мог работать.

3. Оптимизации

В вашем Dockerfile предпочитайте COPY вместо использования ADD, если вы не пытаетесь добавить tar-файлы для автоматической распаковки, следуя лучшим практикам Docker.

Откажитесь от использования команды start в файле package.json и выполняйте её непосредственно. Поэтому вместо этого:

$ CMD ["npm", "start"]

Вы можете использовать это в вашем Dockerfile:

$ CMD ["node", "server.js"]

Это уменьшает количество запущенных процессов внутри контейнера, а также вызывает сигналы выхода, такие как SIGTERM и SIGINT, которые должны быть получены процессом Node.js вместо npm, подавляя их. (см. подробнее — лучшие практики Docker и Node.js)

Вы также можете использовать флаг --init для оборачивания вашего процесса Node.js в лёгкую систему инициализации, которая будет реагировать на сигналы ядра, такие как SIGTERM (CTRL-C) и т.д. Например, вы можете сделать так:

$ docker run --rm -it --init -p 8080:8080 -v $(pwd):/app \
node-docker-dev bash

4. Обслуживание статических файлов

В приведённом выше Dockerfile предполагается, что вы используете API-сервер на Node.js. Допустим, вы хотите обслуживать приложение на React.js/Vue.js/Angular, используя Node.js.

Как вы можете увидеть выше, мы используем пакет serve для обслуживания статических файлов. Предполагая, что вы создаёте UI-приложение с помощью React/Vue/Angular, вы в идеале собираете окончательный бандл, используя npm run build, который будет создавать минифицированные JS и CSS-файлы.

Другой альтернативой является либо: 1) собирать файлы локально и использовать nginx docker для обслуживания этих статических файлов или 2) использовать конвейер (pipeline) CI/CD.

5. Одноэтапная продакшен-сборка

Соберите и запустите образ “всё в одном”:

$ cd node-docker
$ docker build -t node-docker-prod .
$ docker run --rm -it -p 8080:8080 node-docker-prod

Созданный образ будет весит приблизительно 700 Мб (в зависимости от вашей кодовой базы) из-за основного слоя Debian. Давайте посмотрим, как мы можем уменьшить размер.

6. Многоэтапная продакшен-сборка

С многоэтапной сборкой вы используете несколько выражений FROM в своём Dockerfile, но окончательный этап сборки будет использовать только одно из них, и в идеале это будет крошечный продакшен-образ с точно указанными зависимостями, требуемыми в продакшен-сервере.

В вышеприведённом сниппете образ, собранный с Alpine, занимает около 70 Мб, тем самым уменьшая размер в 10 раз. Вариант с использованием alpine обычно является очень безопасным выбором для уменьшения размеров образов.

Есть предложения по улучшению руководства? Или другие варианты использования, которые вы хотели бы видеть? Дайте мне знать в комментариях.

Присоединяйтесь к дискуссии на Reddit или HackerNews :)


Слушайте наш подкаст в iTunes и SoundCloud, читайте нас на Medium, контрибьютьте на GitHub, общайтесь в группе Telegram, следите в Twitter и канале Telegram, рекомендуйте в VK и Facebook.