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

Начиная

Настроим репозиторий. Затем мы поэкспериментируем с некоторыми скриптами npm, чтобы упростить разработку.
В новой папке запустите следующее:

npm init -y

Флаг -y означает «да» или «да для всех». Когда вы используете его, он позволяет вам инициализировать новый файл package.json со всеми значениями по умолчанию без каких-либо запросов пользователя. Это означает, что вы автоматически принимаете все настройки по умолчанию для вашей конфигурации package.json.
Теперь давайте установим Express в качестве зависимости. Нам это понадобится в файле server.js, который мы создадим заранее.

npm i express

Теперь создайте новый файл —server.js в корне этого репозитория. Чтобы сосредоточиться в этом пошаговом руководстве на сценариях npm, мы создадим базовое приложение Node.js, используя платформу Express, чтобы создать простой HTTP-сервер, который выполняет следующие действия:

  1. Импортирует модуль Express и создает приложение Express.
  2. Устанавливает порт сервера на порт 4005 . Мы также можем использовать переменную окружения PORT с помощью process.env или по умолчанию 4005.
  3. Определяет сообщение, которое будет отправлено в качестве ответа клиентам, когда они обращаются к корневому пути ('/').
  4. Настраивает маршрут для обработки запросов HTTP GET к корневому пути. При доступе он записывает сообщение в консоль сервера и отправляет определенное сообщение в качестве ответа.
  5. Запускает сервер на указанном порту, и как только сервер запускается, он записывает определенное сообщение в консоль.

server.js будет выглядеть так:

const express = require('express');
const app = express();
const port = process.env.PORT || 4005;
let runningMessage = 'Server is running on port ' + port;

app.get('/', (req, res) => {
    console.log('API was requested');
    res.send(runningMessage);
    }
);

const server = app.listen(port, () => {
    console.log(runningMessage);
});

Теперь давайте создадим Dockerfile, который устанавливает образ Docker для нашего приложения. Он устанавливает зависимости приложения, копирует код приложения и указывает команду по умолчанию для запуска приложения Node.js с использованием node server.js при открытии порта 4005 внутри контейнера. Чтобы запустить приложение, нам нужно собрать образ из этого Dockerfile, а затем создать и запустить контейнер на основе этого образа. В корневом каталоге создайте Dockerfile:

FROM node:18.10.0

# Create app directory
WORKDIR /usr/src/app

# Install app dependencies
COPY package*.json ./

RUN npm install

# Bundle app source
COPY . .

# Expose port
EXPOSE 4005

# Run app
CMD [ "node", "server.js" ]

Создайте .dockerignore в корневой папке:

node_modules
npm-debug.log

Файл .dockerignore необходим для управления содержимым образа Docker, уменьшения его размера, повышения безопасности и предотвращения непреднамеренного раскрытия конфиденциальной информации.
Теперь, когда все настроено, давайте добавим два основных сценария в файл package.json. файл. Под объектом scripts добавьте следующие сценарии npm:

"docker:build": "docker build -t npm_docker .",
"docker:run": "docker run -p 4005:4005 -d --name npm_d npm_docker",

Флаг -t в команде docker:build используется для пометки изображения именем (в данном случае npm_docker). Это имя будет использоваться для ссылки на образ при запуске контейнеров на его основе.

В команде docker:run -p 4005:4005сопоставляет порт 4005 с хоста с портом 4005 в контейнере. Это позволяет отправлять трафик с хост-компьютера в приложение, работающее внутри контейнера. -d указывает, что контейнер работает в автономном режиме, то есть он будет работать в фоновом режиме как демон. Флаг --name дает контейнеру имя (npm_d), которое можно использовать для управления и взаимодействия с контейнером позже.

Соглашение об именовании

Здесь мы используем соглашение об именах, такое как category:name, поскольку оно обеспечивает ясность, организацию и согласованность в файле package.json.

В контексте docker:build и docker:run это означает намерение и отношение скриптов к операциям Docker. Префикс docker: явно указывает на то, что эти скрипты относятся к задачам Docker.

Это соглашение помогает избежать конфликтов имен, повышает удобочитаемость и позволяет легко группировать и выполнять связанные сценарии. Следуя этому соглашению, разработчики могут легко понимать и управлять сборкой Docker и выполнением задач в рамках проекта.

Чтобы узнать больше о соглашениях об именах, прочитайте это: package-json-conventions.

Задачи разработки Docker

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

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

Для этого добавим еще два скрипта. мы назовем его docker:stop и docker:remove, чтобы он соответствовал нашему соглашению об именах.
Теперь обновитеscripts в package.json следующим образом:

...
"scripts": {
    "docker:stop": "docker container stop npm_d",
    "docker:remove": "docker container rm npm_d",
    "docker:build": "docker build -t npm_docker .",
    "docker:run": "docker run -p 4005:4005 -d --name npm_d npm_docker",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
...

Теперь, когда вы вносите какие-либо изменения в свой server.js, вы можете собрать только что обновленный контейнер, но вы не можете запустить его, так как контейнер с таким именем уже запущен. Следовательно, вам обязательно нужно запустить docker:stop и docker:remove перед запуском команды docker:run.

Автоматизация задач разработки Docker

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

Именно тогда на помощь приходит другой аргумент, доступный через инструмент Docker CLI, который называется «монтирование тома».

Давайте быстро обновим наш scripts в package.json и посмотрим, что делает монтирование тома.

...
"scripts": {
    "docker:stop": "docker container stop npm_d",
    "docker:remove": "docker container rm npm_d",
    "docker:build": "docker build -t npm_docker .",
    "docker:run": "docker run -p 4005:4005 -d --name npm_d npm_docker",
    "docker:run:dev": "docker run -p 4005:4005 -d -v %cd%:/usr/src/app --name npm_d npm_docker",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
...

Сценарий в docker:run:dev почти аналогичен сценарию в docker:run, за исключением аргумента -v. Эта опция монтирует текущий рабочий каталог (%cd%) с хоста в каталог /usr/src/app внутри контейнера.

ПРИМЕЧАНИЕ. Путь слева от двоеточия (:) — это путь к расположению вашего исходного кода. На компьютере с Windows мы можем использовать %cd% , а на компьютере с Linux мы можем использовать $(pwd) для получения рабочего каталога.
Путь справа от двоеточия (:) — это путь к каталогу внутри контейнера Docker. куда вы хотите смонтировать или отобразить исходный код. Этот процесс накладывает ваш локальный исходный код с хост-компьютера на контейнер, обеспечивая динамические обновления всякий раз, когда вы вносите изменения в свой код.

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

Nodemon — это утилита, которая отслеживает ваше приложение Node.js на наличие изменений в файлах. Он автоматически перезапускает сервер Node.js при обнаружении любых изменений в вашей кодовой базе. Эта функция имеет решающее значение для процесса разработки, поскольку избавляет вас от необходимости вручную останавливать и перезапускать сервер каждый раз, когда вы вносите изменения.

Теперь в Dockerfile нам нужно внести два изменения:

  1. Чтобы обеспечить доступность Nodemon в контейнере, мы устанавливаем его глобально. В отличие от разработки, где Nodemon обычно устанавливается как dev-зависимость, здесь он нужен для производственных систем внутри контейнера. При глобальной установке Nodemon становится инструментом командной строки, доступным во всем контейнере. Таким образом, мы можем использовать Nodemon для запуска и мониторинга нашего приложения на предмет изменений в рабочей среде.
  2. Чтобы запустить наше приложение с помощью Nodemon внутри контейнера, мы заменим команду node на nodemon для запуска server.js. Кроме того, мы добавим аргумент -L, чтобы указать устаревший режим наблюдения, поскольку мы работаем внутри контейнера.

Обновленный Dockerfile должен выглядеть так:

FROM node:18.10.0

# Create app directory
WORKDIR /usr/src/app

# Install app dependencies
COPY package*.json ./

RUN npm install

RUN npm install -g nodemon

# Bundle app source
COPY . .

# Expose port
EXPOSE 4005

# Run app
CMD [ "nodemon", "-L", "server.js" ]

Благодаря этому изменению наш Dockerfile теперь правильно настроен для использования Nodemon для запуска приложения и отслеживания изменений кода.

Подведение итогов

Перед запуском docker:run:dev нам нужно убедиться, что нет запущенного контейнера с таким же именем. Мы можем запустить сценарии docker:stop и docker:remove, которые мы определили выше, а затем docker:build перед запуском docker:run:dev.

Здесь мы можем использовать операторы && и &.
&& работает как логическое И. Выполняется первая команда, и если она завершается успешно (возвращает код выхода 0), только тогда запускается вторая команда. Такое поведение гарантирует, что последующие команды будут выполняться условно в зависимости от успеха предыдущих. Он позволяет создать последовательность команд, в которой каждый шаг зависит от успешного завершения предыдущего шага.
Использование & в сценариях npm инициирует параллельное выполнение, позволяя второй команде выполняться независимо в фоновом режиме, даже если первая команда все еще выполняется или обнаруживает ошибки.

Теперь, зная это, попробуем изменить scripts в package.json:

...
"scripts": {
    "docker:stop": "docker container stop npm_d",
    "docker:remove": "docker container rm npm_d",
    "docker:build": "docker build -t npm_docker .",
    "docker:run:dev": "docker run -p 4005:4005 -d -v %cd%:/usr/src/app --name npm_d npm_docker",
    "docker:run": "npm run docker:stop & npm run docker:remove & npm run docker:build && npm run docker:run:dev"
  },
...

Здесь мы использовали & между npm run docker:stop, npm run docker:remove и npm run docker:build. Это связано с тем, что даже при любом новом прогоне контейнера с заданным именем (в нашем случае npm_d) не существует, то цепочка команд не должна останавливаться, а процесс сборки должен продолжаться.
Однако между npm run docker:build и npm:docker:run используется && . Это связано с тем, что если по какой-либо причине процесс сборки завершится неудачно, то команда запуска не должна выполняться, и цепочка команд завершается.

Теперь давайте попробуем упростить это еще больше. Мы будем использовать пакет npm-run-all для последовательного запуска npm-скриптов. Установите npm-run all с помощью следующей команды:

npm i npm-run-all

Теперь обратите внимание на изменение scripts из package.json:

...
"scripts": {
    "docker:stop": "docker container stop npm_d || true",
    "docker:remove": "docker container rm npm_d || true",
    "docker:build": "docker build -t npm_docker .",
    "docker:run": "docker run -p 4005:4005 -d -v %cd%:/usr/src/app --name npm_d npm_docker",
    "docker:run:dev": "npm-run-all docker:*"
  },
...

Обратите внимание, как мы использовали docker:* в docker:run:dev. npm-run-all предоставляет универсальный и мощный способ управления и контроля выполнения скриптов npm, снижая сложность и повышая эффективность процесса сборки.

Приведенная выше команда означает, что все сценарии, начинающиеся с префикса docker:, будут выполняться последовательно. Здесь нам пригодилось наше соглашение об именах.
Здесь || true гарантирует, что даже если docker:stop и docker:remove выдают ошибку, цепочка команд не должна останавливаться, а выполнение переходит к команде сборки.

Вот и все, просто запустив docker:run:dev, вы будете готовы к работе. Используя сценарии npm, вы можете легко управлять своими контейнерами Docker и другими повторяющимися задачами, что позволяет вам больше сосредоточиться на своем основном бизнесе и меньше на ручных настройках.

Дополнительные материалы на PlainEnglish.io.

Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter, LinkedIn, YouTube и Discord .