Автор материала, перевод которого мы публикуем сегодня, работает DevOps-инженером. Он говорит, что ему приходится пользоваться Docker. В частности, эта платформа для управления контейнерами применяется на разных этапах жизненного цикла Node.js-приложений. Использование Docker, технологии, которая, в последнее время, является чрезвычайно популярной, позволяет оптимизировать процесс разработки и вывода в продакшн Node.js-проектов.

Сейчас мы публикуем цикл статей о Docker, предназначенных для тех, кто хочет освоить эту платформу для её использования в самых разных ситуациях. Этот же материал сосредоточен, в основном, на профессиональном применении Docker в Node.js-разработке.
Docker — это программа, которая предназначена для организации виртуализации на уровне операционной системы (контейнеризации). В основе контейнеров лежат многослойные образы. Проще говоря, Docker — это инструмент, который позволяет создавать, разворачивать и запускать приложения с использованием контейнеров, независимых от операционной системы, на которой они выполняются. Контейнер включает в себя образ базовой ОС, необходимой для работы приложения, библиотеки, от которых зависит это приложение, и само это приложение. Если на одном компьютере запущено несколько контейнеров, то они пользуются ресурсами этого компьютера совместно. В контейнерах Docker могут быть упакованы проекты, созданные с использованием самых разных технологий. Нас в данном материале интересуют проекты, основанные на Node.js.
Прежде чем упаковать Node.js-проект в контейнер Docker, нам нужно создать этот проект. Сделаем это. Вот файл
Для установки зависимостей проекта выполним команду
Как видите, тут мы описали простой сервер, возвращающий в ответ на запросы к нему некий текст.
Теперь, когда приложение готово, поговорим о том, как упаковать его в контейнер Docker. А именно, речь пойдёт о том, что является важнейшей частью любого проекта, основанного на Docker, о файле Dockerfile.
Dockerfile — это текстовой файл, который содержит инструкции, описывающие создание образа Docker для приложения. Инструкции, находящиеся в этом файле, если не вдаваться в детали, описывают создание слоёв многоуровневой файловой системы, в которой имеется всё то, что нужно приложению для работы. Платформа Docker умеет кэшировать слои образов, что, при повторном использовании слоёв, которые уже есть в кэше, ускоряет процесс сборки образов.
В объектно-ориентированном программировании существует такое понятие, как класс. Классы используются для создания объектов. В Docker образы можно сравнить с классами, а контейнеры можно сравнить с экземплярами образов, то есть — с объектами. Рассмотрим процесс формирования файла Dockerfile, который поможет нам во всём этом разобраться.
Создадим пустой Dockerfile:
Так как мы собираемся собрать контейнер для Node.js-приложения, то первым, что нам нужно поместить в контейнер, будет базовый образ Node, который можно найти на Docker Hub. Мы будем пользоваться LTS-версией Node.js. В результате первой инструкцией нашего Dockerfile будет следующая инструкция:
После этого создадим директорию для нашего кода. При этом, благодаря использованной здесь инструкции
Так как мы используем образ Node, в нём уже будет установлена платформа Node.js и npm. Пользуясь тем, что уже есть в образе, можно организовать установку зависимостей проекта. С использованием флага
Здесь мы выполняем копирование в образ файла
Теперь скопируем файлы проекта в текущую рабочую директорию. Здесь мы будем использовать не инструкцию ADD, а инструкцию COPY. На самом деле, в большинстве случаев рекомендуется отдавать предпочтение инструкции
Инструкция
Контейнеры Docker представляют собой изолированные среды. Это означает, что мы, запустив приложение в контейнере, не сможем взаимодействовать с ним напрямую, не открыв порт, который прослушивает это приложение. Для того чтобы проинформировать Docker о том, что в некоем контейнере имеется приложение, прослушивающее какой-то порт, можно воспользоваться инструкцией EXPOSE.
К настоящему моменту мы, с помощью Dockerfile, описали образ, который будет содержать приложение и всё, что ему нужно для успешного запуска. Добавим теперь в файл инструкцию, которая позволяет запустить приложение. Это — инструкция CMD. Она позволяет указать некую команду с параметрами, которая будет выполнена при запуске контейнера, и, при необходимости, может быть переопределена средствами командной строки.
Вот как будет выглядеть готовый файл Dockerfile:
Мы подготовили файл Dockerfile, содержащий инструкции по сборке образа, на основе которого будет создан контейнер с работающим приложением. Соберём образ, выполнив команду следующего вида:
В нашем случае она будет выглядеть так:
В Dockerfile есть инструкция
После сборки образа проверим, видит ли его Docker. Для этого выполним такую команду:
В ответ на эту команду должно быть выведено примерно следующее.

Образы Docker
После того, как мы собрали образ Docker, мы можем его запустить, то есть — создать его экземпляр, представленный работающим контейнером. Для этого используется команда такого вида:
В нашем случае она будет выглядеть так:
Запросим у системы информацию о работающих контейнерах с помощью такой команды:
В ответ на это система должна вывести примерно следующее:

Контейнеры Docker
Пока всё идёт так, как ожидается, хотя мы пока ещё не пробовали обратиться к приложению, работающему в контейнере. А именно, наш контейнер, имеющий имя
Если контейнер действительно работает, то в ответ на эту команду будет выдано нечто вроде того, что показано на следующем рисунке.

Результат проверки работоспособности контейнера
На основе одного и того же образа, например, на основе только что созданного, можно создать множество контейнеров. Кроме того, можно отправить наш образ в реестр Docker Hub, что даст возможность другим разработчикам загружать наш образ и запускать соответствующие контейнеры у себя. Такой подход упрощает работу с проектами.
Здесь приведены некоторые рекомендации, к которым стоит прислушаться для того, чтобы эффективно использовать возможности Docker и создавать как можно более компактные образы.
В папке проекта, который планируется поместить в контейнер, всегда нужно создавать файл
Рассмотрим пример, когда мы собираем проект для некоей организации. В этом проекте используется множество npm-пакетов, при этом каждый такой пакет может устанавливать дополнительные пакеты, от которых зависит он сам. Выполнение всех этих операций приводит к дополнительным затратам времени в процессе сборки образа (хотя это, благодаря возможностям Docker по кэшированию, не такая уж и большая неприятность). Хуже то, что итоговый образ, содержащий зависимости некоего проекта, получается довольно большим. Тут, если речь идёт о фронтенд-проектах, можно вспомнить о том, что такие проекты обычно обрабатывают с помощью бандлеров наподобие webpack, которые позволяют удобно упаковывать всё, что нужно приложению в продашкне. В результате файлы npm-пакетов для работы такого проекта оказываются ненужными. А это значит, что от таких файлов мы, после сборки проекта с помощью того же webpack, можем избавиться.
Вооружившись этой идеей, попробуем поступить так:
Такой подход нас, однако, не устроит. Как мы уже говорили, инструкции
В этом примере есть лишь одна инструкция
Представим, что мы работаем над фронтенд-проектом, имеющим множество зависимостей, и мы, для сборки этого проекта, используем webpack. При таком подходе мы можем, ради уменьшения размера образа, воспользоваться возможностями Docker по многоступенчатой сборке образов.
При таком подходе итоговый образ оказывается гораздо меньше предыдущего образа, и мы, кроме того, используем образ

Сравнение образов из репозитория Node
Стремитесь к тому, чтобы в ходе сборки ваших образов использовались бы возможности Docker по кэшированию данных. Мы уже обращали внимание на эту возможность, работая с файлом, к которому обращались по имени
Предположим, мы описываем в Dockerfile установку пакетов в образ, созданный на основе базового образа
Когда система будет обрабатывать этот файл, то, если устанавливаемых пакетов много, операции обновления и установки займут немало времени. Для того чтобы улучшить ситуацию, мы решили воспользоваться возможностями Docker по кэшированию слоёв и переписали Dockerfile так:
Теперь, при сборке образа в первый раз, всё идёт как надо, так как кэш пока не сформирован. Представим себе теперь, что нам нужно установить ещё один пакет,
В результате выполнения такой команды
Рекомендуется всегда, когда это возможно, стремиться к минимизации количества слоёв образов, так как каждый слой — это файловая система образа Docker, а это значит, что чем меньше в образе слоёв — тем компактнее он будет. При использовании многоступенчатого процесса сборки образов достигается уменьшение количества слоёв в образе и уменьшение размера образа.
В этом материале мы рассмотрели процесс упаковки Node.js-приложений в контейнеры Docker и работу с такими контейнерами. Кроме того, мы привели некоторые рекомендации, которые, кстати, могут быть использованы не только при создании контейнеров для Node.js-проектов.
Уважаемые читатели! Если вы профессионально пользуетесь Docker при работе с Node.js-проектами — просим поделиться рекомендациями по эффективному использованию этой системы с новичками.


Сейчас мы публикуем цикл статей о Docker, предназначенных для тех, кто хочет освоить эту платформу для её использования в самых разных ситуациях. Этот же материал сосредоточен, в основном, на профессиональном применении Docker в Node.js-разработке.
Что такое Docker?
Docker — это программа, которая предназначена для организации виртуализации на уровне операционной системы (контейнеризации). В основе контейнеров лежат многослойные образы. Проще говоря, Docker — это инструмент, который позволяет создавать, разворачивать и запускать приложения с использованием контейнеров, независимых от операционной системы, на которой они выполняются. Контейнер включает в себя образ базовой ОС, необходимой для работы приложения, библиотеки, от которых зависит это приложение, и само это приложение. Если на одном компьютере запущено несколько контейнеров, то они пользуются ресурсами этого компьютера совместно. В контейнерах Docker могут быть упакованы проекты, созданные с использованием самых разных технологий. Нас в данном материале интересуют проекты, основанные на Node.js.
Создание Node.js-проекта
Прежде чем упаковать Node.js-проект в контейнер Docker, нам нужно создать этот проект. Сделаем это. Вот файл
package.json
этого проекта:{
"name": "node-app",
"version": "1.0.0",
"description": "The best way to manage your Node app using Docker",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"author": "Ankit Jain <ankitjain28may77@gmail.com>",
"license": "ISC",
"dependencies": {
"express": "^4.16.4"
}
}
Для установки зависимостей проекта выполним команду
npm install
. В ходе работы этой команды, кроме прочего, будет создан файл package-lock.json
. Теперь создадим файл index.js
, в котором будет находиться код проекта:const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('The best way to manage your Node app using Docker\n');
});
app.listen(3000);
console.log('Running on http://localhost:3000');
Как видите, тут мы описали простой сервер, возвращающий в ответ на запросы к нему некий текст.
Создание файла Dockerfile
Теперь, когда приложение готово, поговорим о том, как упаковать его в контейнер Docker. А именно, речь пойдёт о том, что является важнейшей частью любого проекта, основанного на Docker, о файле Dockerfile.
Dockerfile — это текстовой файл, который содержит инструкции, описывающие создание образа Docker для приложения. Инструкции, находящиеся в этом файле, если не вдаваться в детали, описывают создание слоёв многоуровневой файловой системы, в которой имеется всё то, что нужно приложению для работы. Платформа Docker умеет кэшировать слои образов, что, при повторном использовании слоёв, которые уже есть в кэше, ускоряет процесс сборки образов.
В объектно-ориентированном программировании существует такое понятие, как класс. Классы используются для создания объектов. В Docker образы можно сравнить с классами, а контейнеры можно сравнить с экземплярами образов, то есть — с объектами. Рассмотрим процесс формирования файла Dockerfile, который поможет нам во всём этом разобраться.
Создадим пустой Dockerfile:
touch Dockerfile
Так как мы собираемся собрать контейнер для Node.js-приложения, то первым, что нам нужно поместить в контейнер, будет базовый образ Node, который можно найти на Docker Hub. Мы будем пользоваться LTS-версией Node.js. В результате первой инструкцией нашего Dockerfile будет следующая инструкция:
FROM node:8
После этого создадим директорию для нашего кода. При этом, благодаря использованной здесь инструкции
ARG
, мы сможем, при необходимости, указать имя директории приложения, отличное от /app
, во время сборки контейнера. Подробности об этой инструкции можно почитать здесь.# Папка приложения
ARG APP_DIR=app
RUN mkdir -p ${APP_DIR}
WORKDIR ${APP_DIR}
Так как мы используем образ Node, в нём уже будет установлена платформа Node.js и npm. Пользуясь тем, что уже есть в образе, можно организовать установку зависимостей проекта. С использованием флага
--production
(или в том случае, если переменная среды NODE_ENV
установлена в значение production
) npm не будет устанавливать модули, перечисленные в разделе devDependencies
файла package.json
.# Установка зависимостей
COPY package*.json ./
RUN npm install
# Для использования в продакшне
# RUN npm install --production
Здесь мы выполняем копирование в образ файла
package*.json
вместо того, чтобы, например, скопировать все файлы проекта. Мы поступаем именно так из-за того, что инструкции Dockerfile RUN
, COPY
и ADD
создают дополнительные слои образа, благодаря чему можно задействовать возможности по кэшированию слоёв платформы Docker. При таком подходе, когда мы, в следующий раз, будем собирать похожий образ, Docker выяснит, можно ли повторно использовать слои образов, которые уже есть в кэше, и если это так — воспользуется тем, что уже есть, вместо того, чтобы создавать новые слои. Это позволяет серьёзно экономить время при сборке слоёв в ходе работы над большими проектами, включающими в себя множество npm-модулей.Теперь скопируем файлы проекта в текущую рабочую директорию. Здесь мы будем использовать не инструкцию ADD, а инструкцию COPY. На самом деле, в большинстве случаев рекомендуется отдавать предпочтение инструкции
COPY
.Инструкция
ADD
, в сравнении с COPY
, обладает некоторыми возможностями, которые, тем не менее, нужны не всегда. Например, речь идёт о возможностях по распаковке .tar-архивов и по загрузке файлов по URL.# Копирование файлов проекта
COPY . .
Контейнеры Docker представляют собой изолированные среды. Это означает, что мы, запустив приложение в контейнере, не сможем взаимодействовать с ним напрямую, не открыв порт, который прослушивает это приложение. Для того чтобы проинформировать Docker о том, что в некоем контейнере имеется приложение, прослушивающее какой-то порт, можно воспользоваться инструкцией EXPOSE.
# Уведомление о порте, который будет прослушивать работающее приложение
EXPOSE 3000
К настоящему моменту мы, с помощью Dockerfile, описали образ, который будет содержать приложение и всё, что ему нужно для успешного запуска. Добавим теперь в файл инструкцию, которая позволяет запустить приложение. Это — инструкция CMD. Она позволяет указать некую команду с параметрами, которая будет выполнена при запуске контейнера, и, при необходимости, может быть переопределена средствами командной строки.
# Запуск проекта
CMD ["npm", "start"]
Вот как будет выглядеть готовый файл Dockerfile:
FROM node:8
# Папка приложения
ARG APP_DIR=app
RUN mkdir -p ${APP_DIR}
WORKDIR ${APP_DIR}
# Установка зависимостей
COPY package*.json ./
RUN npm install
# Для использования в продакшне
# RUN npm install --production
# Копирование файлов проекта
COPY . .
# Уведомление о порте, который будет прослушивать работающее приложение
EXPOSE 3000
# Запуск проекта
CMD ["npm", "start"]
Сборка образа
Мы подготовили файл Dockerfile, содержащий инструкции по сборке образа, на основе которого будет создан контейнер с работающим приложением. Соберём образ, выполнив команду следующего вида:
docker build --build-arg <build arguments> -t <user-name>/<image-name>:<tag-name> /path/to/Dockerfile
В нашем случае она будет выглядеть так:
docker build --build-arg APP_DIR=var/app -t ankitjain28may/node-app:V1 .
В Dockerfile есть инструкция
ARG
, описывающая аргумент APP_DIR
. Здесь мы задаём его значение. Если этого не сделать, то он примет то значение, которое присвоено ему в файле, то есть — app
.После сборки образа проверим, видит ли его Docker. Для этого выполним такую команду:
docker images
В ответ на эту команду должно быть выведено примерно следующее.

Образы Docker
Запуск образа
После того, как мы собрали образ Docker, мы можем его запустить, то есть — создать его экземпляр, представленный работающим контейнером. Для этого используется команда такого вида:
docker run -p <External-port:exposed-port> -d --name <name of the container> <user-name>/<image-name>:<tag-name>
В нашем случае она будет выглядеть так:
docker run -p 8000:3000 -d --name node-app ankitjain28may/node-app:V1
Запросим у системы информацию о работающих контейнерах с помощью такой команды:
docker ps
В ответ на это система должна вывести примерно следующее:

Контейнеры Docker
Пока всё идёт так, как ожидается, хотя мы пока ещё не пробовали обратиться к приложению, работающему в контейнере. А именно, наш контейнер, имеющий имя
node-app
, прослушивает порт 8000
. Для того чтобы попытаться к нему обратиться, можно открыть браузер и перейти в нём по адресу localhost:8000
. Кроме того, для того, чтобы проверить работоспособность контейнера, можно воспользоваться такой командой:curl -i localhost:8000
Если контейнер действительно работает, то в ответ на эту команду будет выдано нечто вроде того, что показано на следующем рисунке.

Результат проверки работоспособности контейнера
На основе одного и того же образа, например, на основе только что созданного, можно создать множество контейнеров. Кроме того, можно отправить наш образ в реестр Docker Hub, что даст возможность другим разработчикам загружать наш образ и запускать соответствующие контейнеры у себя. Такой подход упрощает работу с проектами.
Рекомендации
Здесь приведены некоторые рекомендации, к которым стоит прислушаться для того, чтобы эффективно использовать возможности Docker и создавать как можно более компактные образы.
▍1. Всегда создавайте файл .dockerignore
В папке проекта, который планируется поместить в контейнер, всегда нужно создавать файл
.dockerignore
. Он позволяет игнорировать файлы и папки, в которых нет необходимости при сборке образа. При таком подходе мы сможем уменьшить так называемый контекст сборки, что позволит быстрее собрать образ и уменьшить его размер. Этот файл поддерживает шаблоны имён файлов, в этом он похож на файл .gitignore
. Рекомендуется добавить в .dockerignore
команду, благодаря которой Docker проигнорирует папку /.git
, так как в этой папке обычно содержатся материалы большого размера (особенно в процессе разработки проекта) и её добавление в образ ведёт к увеличению его размера. Кроме того, в том, чтобы копировать эту папку в образ, нет особого смысла.▍2. Используйте многоступенчатый процесс сборки образов
Рассмотрим пример, когда мы собираем проект для некоей организации. В этом проекте используется множество npm-пакетов, при этом каждый такой пакет может устанавливать дополнительные пакеты, от которых зависит он сам. Выполнение всех этих операций приводит к дополнительным затратам времени в процессе сборки образа (хотя это, благодаря возможностям Docker по кэшированию, не такая уж и большая неприятность). Хуже то, что итоговый образ, содержащий зависимости некоего проекта, получается довольно большим. Тут, если речь идёт о фронтенд-проектах, можно вспомнить о том, что такие проекты обычно обрабатывают с помощью бандлеров наподобие webpack, которые позволяют удобно упаковывать всё, что нужно приложению в продашкне. В результате файлы npm-пакетов для работы такого проекта оказываются ненужными. А это значит, что от таких файлов мы, после сборки проекта с помощью того же webpack, можем избавиться.
Вооружившись этой идеей, попробуем поступить так:
# Установка зависимостей
COPY package*.json ./
RUN npm install --production
# Продакшн-сборка
COPY . .
RUN npm run build:production
# Удаление папки с npm-модулями
RUN rm -rf node_modules
Такой подход нас, однако, не устроит. Как мы уже говорили, инструкции
RUN
, ADD
и COPY
создают слои, кэшируемые Docker, поэтому нам надо найти способ справиться с установкой зависимостей, сборкой проекта и последующим удалением ненужных файлов с помощью одной команды. Например, это может выглядеть так:# Добавляем в образ весь проект
COPY . .
# Устанавливаем зависимости, собираем проект и удаляем зависимости
RUN npm install --production && npm run build:production && rm -rf node_module
В этом примере есть лишь одна инструкция
RUN
, которая устанавливает зависимости, собирает проект и удаляет папку node_modules
. Это приводит к тому, что размер образа будет не таким большим, как размер образа, включающего в себя папку node_modules
. Мы пользуемся файлами из этой папки только в процессе сборки проекта, после чего удаляем её. Правда, такой подход плох тем, что установка npm-зависимостей занимает много времени. Устранить этот недостаток можно, воспользовавшись технологией многоступенчатой сборки образов.Представим, что мы работаем над фронтенд-проектом, имеющим множество зависимостей, и мы, для сборки этого проекта, используем webpack. При таком подходе мы можем, ради уменьшения размера образа, воспользоваться возможностями Docker по многоступенчатой сборке образов.
FROM node:8 As build
# Папки
RUN mkdir /app && mkdir /src
WORKDIR /src
# Установка зависимостей
COPY package*.json ./
RUN npm install
# Для использования в продакшне
# RUN npm install --production
# Копирование файлов проекта и сборка проекта
COPY . .
RUN npm run build:production
# В результате получается образ, состоящий из одного слоя
FROM node:alpine
# Копируем собранные файлы из папки build в папку app
COPY --from=build ./src/build/* /app/
ENTRYPOINT ["/app"]
CMD ["--help"]
При таком подходе итоговый образ оказывается гораздо меньше предыдущего образа, и мы, кроме того, используем образ
node:alpine
, который и сам по себе очень мал. А вот сравнение пары образов, в ходе которого видно, что образ node:alpine
гораздо меньше, чем образ node:8
.
Сравнение образов из репозитория Node
▍3. Используйте кэш Docker
Стремитесь к тому, чтобы в ходе сборки ваших образов использовались бы возможности Docker по кэшированию данных. Мы уже обращали внимание на эту возможность, работая с файлом, к которому обращались по имени
package*.json
. Это позволяет сократить время сборки образа. Но данной возможностью не стоит пользоваться необдуманно.Предположим, мы описываем в Dockerfile установку пакетов в образ, созданный на основе базового образа
Ubuntu:16.04
:FROM ubuntu:16.04
RUN apt-get update && apt-get install -y \
curl \
package-1 \
.
.
Когда система будет обрабатывать этот файл, то, если устанавливаемых пакетов много, операции обновления и установки займут немало времени. Для того чтобы улучшить ситуацию, мы решили воспользоваться возможностями Docker по кэшированию слоёв и переписали Dockerfile так:
FROM ubuntu:16.04
RUN apt-get update
RUN apt-get install -y \
curl \
package-1 \
.
.
Теперь, при сборке образа в первый раз, всё идёт как надо, так как кэш пока не сформирован. Представим себе теперь, что нам нужно установить ещё один пакет,
package-2
. Для этого мы переписываем файл:FROM ubuntu:16.04
RUN apt-get update
RUN apt-get install -y \
curl \
package-1 \
package-2 \
.
.
В результате выполнения такой команды
package-2
не будет установлен или обновлён. Почему? Дело в том, что при выполнении инструкции RUN apt-get update
, Docker не видит никакой разницы этой инструкции и инструкции, выполнявшейся ранее, в результате он берёт данные из кэша. А эти данные уже устарели. При обработке инструкции RUN apt-get install
система выполняет её, для неё она выглядит не так, как похожая инструкция в предыдущем Dockerfile, но в ходе установки могут либо возникнуть ошибки, либо установлена будет старая версия пакетов. В результате оказывается, что команды update
и install
нужно выполнять в рамках одной инструкции RUN
, так, как сделано в первом примере. Кэширование — это замечательная возможность, но необдуманное использования этой возможности может приводить к проблемам.▍4. Минимизируйте количество слоёв образов
Рекомендуется всегда, когда это возможно, стремиться к минимизации количества слоёв образов, так как каждый слой — это файловая система образа Docker, а это значит, что чем меньше в образе слоёв — тем компактнее он будет. При использовании многоступенчатого процесса сборки образов достигается уменьшение количества слоёв в образе и уменьшение размера образа.
Итоги
В этом материале мы рассмотрели процесс упаковки Node.js-приложений в контейнеры Docker и работу с такими контейнерами. Кроме того, мы привели некоторые рекомендации, которые, кстати, могут быть использованы не только при создании контейнеров для Node.js-проектов.
Уважаемые читатели! Если вы профессионально пользуетесь Docker при работе с Node.js-проектами — просим поделиться рекомендациями по эффективному использованию этой системы с новичками.
