Перевод статьи Praveen Durairaj: An Exhaustive Guide to Writing Dockerfiles for Node.js Web Apps. Опубликовано с разрешения автора.
Данный пост состоит из нескольких примеров, начиная от простого Dockerfile до многоэтапных продакшен-сборок для веб-приложений Node.js. Краткое описание того, что охватывает данное руководство:
- Использование соответствующего базового образа (carbon для разработки, alpine для продакшена).
- Использование nodemon для горячей перезагрузки во время разработки.
- Оптимизация для уровней кеша Docker — размещение команд в правильном порядке, так что
npm install
выполняется только при необходимости. - Обслуживание статических файлов (бандлов, созданных React/Vue/Angular), используя пакет
serve
. - Использование многоэтапной сборки
alpine
для уменьшения окончательной продакшен-сборки. - Советы профи: 1) Использование COPY вместо ADD 2) Обработка сигналов ядра (kernel signals) при нажатии CTRL-C при помощи флага
init
Если вы сразу хотите перейти к коду, посмотрите репозиторий на GitHub.
- Простой Dockerfile и .dockerignore
- Горячая перезагрузка с nodemon
- Оптимизации
- Обслуживание статических файлов
- Одноэтапная продакшен-сборка
- Многоэтапная продакшен-сборка
Давайте представим простую структуру каталогов. Наше приложение будет называться node-app
. В каталоге верхнего уровня есть два файла: Dockerfile
и package.json
. Исходный код node-приложения будет находятся в каталоге src
. Для краткости предположим, что файл server.js
содержит код Express-сервера, запущенного на порте 8080
.
node-app
├── Dockerfile
├── package.json
└── src
└── server.js
Для базового образа мы использовали последнюю 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
nodemon — популярный пакет, который будет отслеживать файлы в каталоге, в котором он был запущен. Если какие-нибудь файлы будут изменены, то nodemon автоматически перезагрузит ваше приложение.
<script src="https://gist.github.com/lex111/f05fcce2c9e8b1dcd17cd923c955cb6b.js"></script>Мы соберём образ и запустим 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 мог работать.
В вашем 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
В приведённом выше Dockerfile предполагается, что вы используете API-сервер на Node.js. Допустим, вы хотите обслуживать приложение на React.js/Vue.js/Angular, используя Node.js.
<script src="https://gist.github.com/lex111/e8e50c64a67664e2a7d653517cd384ae.js"></script>Как вы можете увидеть выше, мы используем пакет serve для обслуживания статических файлов. Предполагая, что вы создаёте UI-приложение с помощью React/Vue/Angular, вы в идеале собираете окончательный бандл, используя npm run build
, который будет создавать минифицированные JS и CSS-файлы.
Другой альтернативой является либо: 1) собирать файлы локально и использовать nginx docker для обслуживания этих статических файлов или 2) использовать конвейер (pipeline) CI/CD.
<script src="https://gist.github.com/lex111/e66a33435c0576d1feab13f3a7c99e61.js"></script>Соберите и запустите образ "всё в одном":
$ cd node-docker
$ docker build -t node-docker-prod .
$ docker run --rm -it -p 8080:8080 node-docker-prod
Созданный образ будет весит приблизительно 700 Мб (в зависимости от вашей кодовой базы) из-за основного слоя Debian. Давайте посмотрим, как мы можем уменьшить размер.
С многоэтапной сборкой вы используете несколько выражений FROM
в своём Dockerfile, но окончательный этап сборки будет использовать только одно из них, и в идеале это будет крошечный продакшен-образ с точно указанными зависимостями, требуемыми в продакшен-сервере.
В вышеприведённом сниппете образ, собранный с Alpine, занимает около 70 Мб, тем самым уменьшая размер в 10 раз. Вариант с использованием alpine
обычно является очень безопасным выбором для уменьшения размеров образов.
Есть предложения по улучшению руководства? Или другие варианты использования, которые вы хотели бы видеть? Дайте мне знать в комментариях.
Присоединяйтесь к дискуссии на Reddit или HackerNews :)
Слушайте наш подкаст в iTunes и SoundCloud, читайте нас на Medium, контрибьютьте на GitHub, общайтесь в группе Telegram, следите в Twitter и канале Telegram, рекомендуйте в VK и Facebook.