Контейнеризация приложения Node.js для разработки: определение сервисов с помощью Docker Compose

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

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

В этой серии мануалов вы узнаете, как настроить среду разработки для приложения Node.js с помощью Docker. Поскольку это приложение использует Node и MongoDB, такая установка будет делать следующее:

  • Синхронизировать код приложения на хосте с кодом в контейнере (чтобы упростить изменения во время разработки).
  • Проверять, работают ли изменения в коде приложения без перезапуска.
  • Создавать пользовательскую и защищенную паролем БД для данных приложения.
  • Сохранять эти данные.

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

Примечание: Прежде чем приступить к работе, выполните первую часть этой серии – мануал Контейнеризация приложения Node.js для разработки: базовая настройка.

1: Определение сервисов с помощью Docker Compose

Сейчас нужно создать файл docker-compose.yml с вашими определениями сервиса. Сервис в Compose – это работающий контейнер, а определения сервисов, которые вы включите в файл docker-compose.yml, содержат информацию о том, как будет работать каждый образ контейнера. Инструмент Compose позволяет определить несколько сервисов для создания мультиконтейнерных приложений.

Однако перед определением сервисов мы добавим в проект новый инструмент под названием wait-for, с помощью которого приложение будет пытаться подключиться к базе данных только после завершения задач запуска БД. Этот скрипт-обертка использует netcat, чтобы проверить, принимают ли конкретный хост и порт TCP-соединения. С его помощью вы можете контролировать попытки приложения подключиться к базе данных, проверяя, готова ли БД принимать подключения.

Читайте также: Использование Netcat для создания и тестирования соединений TCP и UDP на VPS

Хотя Compose позволяет указывать зависимости между сервисами с помощью опции depends_on, этот порядок основан не на готовности контейнера, а на том, работает ли он. Опция depends_on не будет оптимальной для нашей установки, поскольку мы хотим, чтобы приложение подключалось только после завершения задач запуска БД (включая добавление пользователя и пароля в БД аутентификации admin). Для получения дополнительной информации об использовании wait-for и других инструментов управления запуском, пожалуйста, ознакомьтесь с рекомендациями в документации Compose.

Откройте wait-for.sh:

nano wait-for.sh

Вставьте в файл следующие строки, чтобы создать опрашивающую функцию:

#!/bin/sh
# original script: https://github.com/eficode/wait-for/blob/master/wait-for
TIMEOUT=15
QUIET=0
echoerr() {
if [ "$QUIET" -ne 1 ]; then printf "%s\n" "$*" 1>&2; fi
}
usage() {
exitcode="$1"
cat << USAGE >&2
Usage:
$cmdname host:port [-t timeout] [-- command args]
-q | --quiet                        Do not output any status messages
-t TIMEOUT | --timeout=timeout      Timeout in seconds, zero for no timeout
-- COMMAND ARGS                     Execute command with args after the test finishes
USAGE
exit "$exitcode"
}
wait_for() {
for i in `seq $TIMEOUT` ; do
nc -z "$HOST" "$PORT" > /dev/null 2>&1
result=$?
if [ $result -eq 0 ] ; then
if [ $# -gt 0 ] ; then
exec "$@"
fi
exit 0
fi
sleep 1
done
echo "Operation timed out" >&2
exit 1
}
while [ $# -gt 0 ]
do
case "$1" in
*:* )
HOST=$(printf "%s\n" "$1"| cut -d : -f 1)
PORT=$(printf "%s\n" "$1"| cut -d : -f 2)
shift 1
;;
-q | --quiet)
QUIET=1
shift 1
;;
-t)
TIMEOUT="$2"
if [ "$TIMEOUT" = "" ]; then break; fi
shift 2
;;
--timeout=*)
TIMEOUT="${1#*=}"
shift 1
;;
--)
shift
break
;;
--help)
usage 0
;;
*)
echoerr "Unknown argument: $1"
usage 1
;;
esac
done
if [ "$HOST" = "" -o "$PORT" = "" ]; then
echoerr "Error: you need to provide a host and port to test."
usage 2
fi
wait_for "$@"

Сохраните и закройте этот файл.

Сделайте скрипт исполняемым:

chmod +x wait-for.sh

Откройте файл docker-compose.yml:

nano docker-compose.yml

Сначала определите сервис приложения nodejs, добавив в файл следующий код:

version: '3'
services:
nodejs:
build:
context: .
dockerfile: Dockerfile
image: nodejs
container_name: nodejs
restart: unless-stopped
env_file: .env
environment:
- MONGO_USERNAME=$MONGO_USERNAME
- MONGO_PASSWORD=$MONGO_PASSWORD
- MONGO_HOSTNAME=db
- MONGO_PORT=$MONGO_PORT
- MONGO_DB=$MONGO_DB
ports:
- "80:8080"
volumes:
- .:/home/node/app
- node_modules:/home/node/app/node_modules
networks:
- app-network
command: ./wait-for.sh db:27017 -- /home/node/app/node_modules/.bin/nodemon app.js

Определение сервиса nodejs включает в себя следующие параметры:

  • build: определяет параметры конфигурации, включая context  и dockerfile, которые будут применяться при сборке образа приложения Compose. Если вы хотите использовать существующий образ из реестра, например из Docker Hub, вы можете вместо этого использовать опцию image с информацией о пользователе, хранилище и теге образа.
  • context: определяет контекст для сборки образа – в данном случае это текущий каталог проекта.
  • dockerfile: указывает Dockerfile в текущем каталоге проекта, который Compose будет использовать для создания образа приложения. Больше информации об этом файле вы найдете в мануале Сборка приложения Node.js с помощью Docker.
  • image, container_name: присваивают имена образу и контейнеру.
  • restart: определяет политику перезапуска. По умолчанию имеет знаение no, но мы настроили перезапуск контейнера, если он не остановлен.
  • env_file: позволяет Compose добавить переменные среды из файла.env, расположенного в контексте сборки.
  • environment: позволяет добавить параметры подключения Mongo, которые вы определили в файле .env. Обратите внимание, мы не устанавливаем в NODE_ENV значение development, поскольку это поведение по умолчанию в Express, если значение NODE_ENV не установлено. При переходе в рабочий режим вы можете установить здесь значение production, чтобы включить кэширование представления и уменьшить количество подробных сообщений об ошибках. Также обратите внимание, что мы указали контейнер базы данных db в качестве хоста, как обсуждалось в разделе 2.
  • ports: связывает порт 80 на хосте с портом 8080 в контейнере.
  • volumes: мы включаем два типа монтирования:
    • Первый тип – это подключение, которое монтирует код приложения на хосте в каталог /home/node/app контейнера. Это облегчит разработку, так как все изменения, которые вы вносите в код хоста, будут немедленно добавлены в контейнер.
    • Вторым типом является именованный том, node_modules. Когда Docker запускает команду npm install, указанную в Dockerfile приложения, npm создаст в контейнере новый каталог node_modules, который включает необходимые пакеты для запуска приложения. Однако только что созданное монтирование будет скрывать этот недавно созданный каталог node_modules. Так как node_modules на хосте пуст, пустой каталог будет передан в контейнер, переопределив новый каталог node_modules и предотвратив запуск приложения. Именованный том node_modules решает эту проблему, сохраняя содержимое каталога /home/node/app/node_modules и монтируя его в контейнер, скрывая связку.
  • При этом подходе следует помнить следующее:
    • Связка смонтирует содержимое каталога node_modules в контейнере на хост, этот каталог будет принадлежать root, так как том был создан Docker.
    • Если у вас есть существующий каталог node_modules на хосте, он переопределит каталог node_modules в контейнере. Такая настройка предполагает, что пока что каталог node_modules не существует и что вы не будете работать с npm на своем хосте. Это соответствует «12 факторам» разработки приложений, поскольку минимизирует зависимости между средами исполнения.
  • networks: указывает, что сервис приложений будет подключаться к сети app-network, которую мы определим в конце файла.
  • command: позволяет установить команду, которая должна выполняться, когда Compose запускает образ. Обратите внимание, это переопределит инструкцию CMD, которую мы установили в Dockerfile приложения. Здесь мы запускаем приложение с помощью сценария wait-for, который опрашивает сервис db по порту 27017, чтобы понять, готов ли он. После успешного тестирования скрипт выполнит указанную команду, чтобы запустить приложение с помощью nodemon. При этом все будущие изменения, которые вы внесете в код, будут загружены автоматически (без необходимости перезапускать приложение).

Затем создайте сервис db, добавив следующий код под определение сервиса приложения:

...
db:
image: mongo:4.1.8-xenial
container_name: db
restart: unless-stopped
env_file: .env
environment:
- MONGO_INITDB_ROOT_USERNAME=$MONGO_USERNAME
- MONGO_INITDB_ROOT_PASSWORD=$MONGO_PASSWORD
volumes:
- dbdata:/data/db
networks:
- app-network

Некоторые параметры, которые мы определили для сервиса nodejs, остались прежними, но мы внесли следующие изменения в определения image, environment и volumes:

  • image: чтобы создать этот сервис, Compose будет извлекать образ Mongo 4.1.8-xenial из Docker Hub. Мы указываем конкретную версию, чтобы избежать возможных будущих конфликтов при изменении образа Mongo. Читайте документацию Docker о передовых методах Dockerfile.
  • MONGO_INITDB_ROOT_USERNAME, MONGO_INITDB_ROOT_PASSWORD: образ mongo делает эти переменные среды доступными, чтобы вы могли изменить инициализацию своего экземпляра базы данных. Вместе MONGO_INITDB_ROOT_USERNAME и MONGO_INITDB_ROOT_PASSWORD создают пользователя root в базе данных admin и обеспечивают аутентификацию при запуске контейнера. Мы установили MONGO_INITDB_ROOT_USERNAME и MONGO_INITDB_ROOT_PASSWORD, используя значения из файла .env, которые мы передаем в dbservice с помощью опции env_file. Это означает, что пользователь приложения 8host будет root в экземпляре БД (с доступом ко всем административным и операционным привилегиям этой роли). В рабочей среде вы можете создать отдельного пользователя приложения с соответствующими правами доступа. Имейте в виду: эти переменные не вступят в силу, если вы запустите контейнер с существующим каталогом данных.
  • dbdata:/data/db: именованный том dbdata сохраняет данные, хранящиеся в каталоге данных Mongo по умолчанию, /data/db. Благодаря этому вы не потеряете данные при остановке или удалении контейнера.

Мы также добавили сервис db в сеть app-network с опцией network.

В конец файла добавьте определения томов и сети:

...
networks:
app-network:
driver: bridge
volumes:
dbdata:
node_modules:

Пользовательская мостовая сеть app-network обеспечивает связь между контейнерами, поскольку они находятся на одном хосте демона Docker. Это оптимизирует трафик и обмен данными внутри приложения, поскольку открывает все порты между контейнерами в одной и той же мостовой сети, но не открывает порты для внешнего мира. Таким образом, наши контейнеры db и nodejs могут взаимодействовать друг с другом, нужно только открыть порт 80 для внешнего доступа к приложению.

Ключ верхнего уровня volumes определяет тома dbdata и node_modules. Когда Docker создает тома, его содержимое хранится в части файловой системы хоста, /var/lib/docker/volumes/, которая управляется Docker. Содержимое каждого тома хранится в каталоге в /var/lib/docker/volumes/ и монтируется к любому контейнеру, использующему том. Таким образом, данные, которые будут создавать наши пользователи, будут сохраняться в томе dbdata, даже если мы удалим и заново создадим контейнер db.

Готовый файл docker-compose.yml будет выглядеть так:

version: '3'
services:
nodejs:
build:
context: .
dockerfile: Dockerfile
image: nodejs
container_name: nodejs
restart: unless-stopped
env_file: .env
environment:
- MONGO_USERNAME=$MONGO_USERNAME
- MONGO_PASSWORD=$MONGO_PASSWORD
- MONGO_HOSTNAME=db
- MONGO_PORT=$MONGO_PORT
- MONGO_DB=$MONGO_DB
ports:
- "80:8080"
volumes:
- .:/home/node/app
- node_modules:/home/node/app/node_modules
networks:
- app-network
command: ./wait-for.sh db:27017 -- /home/node/app/node_modules/.bin/nodemon app.js
db:
image: mongo:4.1.8-xenial
container_name: db
restart: unless-stopped
env_file: .env
environment:
- MONGO_INITDB_ROOT_USERNAME=$MONGO_USERNAME
- MONGO_INITDB_ROOT_PASSWORD=$MONGO_PASSWORD
volumes:
- dbdata:/data/db
networks:
- app-network
networks:
app-network:
driver: bridge
volumes:
dbdata:
node_modules:

Сохраните и закройте файл.

Теперь все готово к запуску приложения.

2: Тестирование приложения

С помощью docker-compose.yml и команды docker-compose up вы можете создавать свои сервисы. Вы также можете проверить, сохраняются ли ваши данные, остановив и удалив контейнеры с помощью команды docker-compose down.

Сначала создайте образы контейнеров и создайте сервисы, запустив docker-compose с параметром –d, который запустит контейнеры nodejs и db в фоновом режиме:

docker-compose up -d

Вы увидите вывод, подтверждающий, что ваши услуги были созданы:

...
Creating db ... done
Creating nodejs ... done

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

docker-compose logs

Вы увидите что-то вроде такого вывода, если все работает правильно:

...
nodejs    | [nodemon] starting `node app.js`
nodejs    | Example app listening on 8080!
nodejs    | MongoDB is connected
...
db        | 2019-02-22T17:26:27.329+0000 I ACCESS   [conn2] Successfully authenticated as principal 8host on admin

Вы также можете проверить состояние ваших контейнеров с помощью docker-compose ps:

docker-compose ps

Вы увидите такой вывод, если ваши контейнеры работают:

Name               Command               State          Ports
----------------------------------------------------------------------
db        docker-entrypoint.sh mongod      Up      27017/tcp
nodejs   ./wait-for.sh db:27017 --  ...   Up      0.0.0.0:80->8080/tcp

Когда сервисы запустятся, вы можете открыть в браузере http://your_server_ip. Вы увидите целевую страницу, которая выглядит следующим образом:

Want to Learn About Sharks?
Are you ready to learn about sharks?
Get Shark Info

Нажмите на кнопку Get Shark Info. Вы увидите страницу Shark Info с маленькой формой ввода Enter your shark в правой части экрана. Форма состоит из полей Shark Name и Shark Character.

Чтобы убедиться, что все работает правильно, добавьте в это поле информацию о любой акуле. Например, можно попробовать ввести в первое поле Megalodon Shark, а во второе — Ancient

Нажмите на кнопку Submit. Вы увидите, что добавленная вами информация появилась на странице.

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

Вернувшись в свой терминал, введите следующую команду, чтобы остановить и удалить контейнеры и сеть:

docker-compose down

Обратите внимание, мы не включаем опцию —volumes; следовательно, том dbdata не удаляется.

Следующий вывод подтверждает, что контейнеры и сеть были удалены:

Stopping nodejs ... done
Stopping db     ... done
Removing nodejs ... done
Removing db     ... done
Removing network node_project_app-network

Восстановите контейнеры:

docker-compose up -d

Теперь вернитесь к форме ввода пользовательских данных. Введите новые данные о другой акуле, например, Whale Shark и Large.

Как только вы нажмете Submit, вы увидите, что новая акула была добавлена в коллекцию в базе данных, но предыдущие данные при этом не были потеряны.

Теперь приложение работает в контейнерах Docker с включенным хранением данных и синхронизацией кода.

Заключение

Следуя этому мануалу, вы создали среду разработки для приложения Node с использованием контейнеров Docker. Вы сделали свой проект модульным и переносимым, извлекли конфиденциальную информацию и отделили состояние приложения от кода. Вы также настроили стандартный файл docker-compose.yml, который вы можете изменять по мере роста потребностей и требований разработки.

Читайте также:

В дальнейшей работе над приложениями вам могут понадобиться знания о рабочих процессах Cloud Native.

Tags: , ,