Контейнеризация приложения Node.js для разработки: определение сервисов с помощью Docker Compose
Ubuntu | Комментировать запись
Если вы активно разрабатываете приложение, 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, который вы можете изменять по мере роста потребностей и требований разработки.
Читайте также:
- Архитектурное проектирование приложений в Kubernetes
- Сборка приложения Node.js с помощью Docker
- Интеграция MongoDB в приложение Node.js
- Защита контейнерного приложения Node.js с помощью Nginx, Let’s Encrypt и Docker Compose
В дальнейшей работе над приложениями вам могут понадобиться знания о рабочих процессах Cloud Native.
Tags: Docker, Docker Compose, Node.js