Ну вот и я добрался до Docker. До этого пытался, но все урывками, и цельная картина не складывалась. Но было принято решение, что свое приложение буду разворачивать в контейнерах. Поэтому сел раскуривать Docker основательно, параллельно конспектируя с целью написать статью. И вот, когда все детали сложились в общую картину, обнаружил, что наконспектировал 30+ страниц. Боюсь, что статья с таким объемом будет тяжела для восприятия, поэтому встречайте небольшой цикл, посвященный контейнеризации MEAN-stack приложений.
Для начала рекомендую пройти этот короткий официальный туториал Docker.
Содержание цикла
- Часть 1 - Dockerfile, образы, контейнеры
- Часть 2 - Docker Compose
- Часть 3 - Traefik reverse proxy
- Часть 4 - Docker Swarm
- Часть 5 - Mongo Replica Set в Docker Swarm
Описание демо-проекта
Демо-приложение не имеет особой ценности и предназначено только для того, чтобы показать работоспособность в целом. Оно должно принимать запросы, сохранять в базу данных своеобразный лог (имя хоста, req.hostname
и x-real-ip
заголовок запроса) и в ответе возвращать 10 последних сохраненных записей.
Структурно демо-приложение будет состоять из следующих основных частей:
- серверная часть
- клиентская часть
- база данных
В дальнейшем, по мере углубления, будут добавлены сервисы reverse proxy и мониторинга.
Сервер
- Express.js 4.x
- Mongoose 5.x
Сгенерирован с помощью express-generator
:
npm install express-generator -g
express --no-view server
Основная логика содержится в этих строках:
router.post('/', function (req, res, next) {
let newData = Data({
host: os.hostname(),
req_host: req.hostname,
req_ip: req.header('x-real-ip')
});
newData.save()
.then((data) => {
return Data.find().sort('-date').limit(10).exec();
})
.then((list) => {
res.json(list)
})
.catch(err => {
return next(err);
});
});
Клиент
- Angular 6.x
- Angular Universal 6.x (SSR)
Сгенерирован с помощью Angular CLI:
npm install -g @angular/cli
ng new client
Настройка серверного рендеринга Angular >>
Кнопка, по нажатию на которую отправляется запрос на сервер, таблица со списком последних сохраненных в БД запросов:
База данных
- MongoDB 3.6
Dockerfile
Это файл, описывающий, как должна производиться сборка Docker-образа с нашим приложением. Для серверного и клиентского приложений потребуется создать два файла с названием Dockerfile
.
/server/Dockerfile
# В качестве исходного используем официальный образ
# диспетчера процессов PM2 на базе ОС Alpine
FROM keymetrics/pm2:latest-alpine
# Создаем рабочую директорию
WORKDIR /app
# Копируем в неё файлы package.json и package-lock.json
COPY package*.json ./
# Выполняем установку пакетов npm
RUN npm install
# Копируем остальные файлы приложения
COPY . ./
# Открываем наружу порт 3000
EXPOSE 3000
# Указываем команду, выполняемую при запуске контейнера
CMD [ "pm2-runtime", "start", "ecosystem.config.js" ]
/client/Dockerfile
Здесь воспользуемся возможностью многостадийной сборки образа Docker:
# Первая стадия сборки (назовем build)
# В качестве исходного используем образ NodeJS на базе ОС Alpine
FROM node:alpine AS build
# Создаем рабочую директорию
WORKDIR /app
# Копируем в неё файлы package.json и package-lock.json
COPY package*.json ./
# Устанавливаем переменную среды для loglevel
# и выполняем установку пакетов npm
ENV NPM_CONFIG_LOGLEVEL warn
RUN npm install
# Копируем остальные файлы приложения
COPY . ./
# Определяем переменную стадии со значением по умолчанию
# Для изменения в build указать --build-arg STAGE=development
ARG STAGE=production
# Выполняем сборку приложения
RUN $(npm bin)/ng build --configuration=$STAGE && \
$(npm bin)/ng run client:ssr:$STAGE && \
npm run webpack:server
# Вторая стадия
# Используем образ PM2
FROM keymetrics/pm2:latest-alpine AS release
# Создаем рабочую директорию
WORKDIR /app
# Копируем из build скомпилированное Angular приложение
# и файл ecosystem
COPY --from=build /app/dist ./dist
COPY --from=build /app/ecosystem.config.js ./
# Шарим порт
EXPOSE 4000
# Команда для запуска
CMD [ "pm2-runtime", "start", "ecosystem.config.js" ]
Многостадийная сборка позволяет значительно уменьшить размер финального образа, так как в него не переносятся установленные пакеты npm.
PM2
Для запуска и управления процессами Node.js приложения используем диспетчер PM2. Создадим его конфигурационный файл:
/server/ecosystem.config.js
module.exports = {
apps: [{
name: "server",
script: "bin/www",
exec_mode: "cluster",
instances: "max",
instance_var: 'INSTANCE_ID',
env: {
NODE_ENV: "development",
},
env_production: {
NODE_ENV: "production",
}
}]
};
Приложение запускаем в режиме кластера, а instances: "max"
указываем, что необходимо запускать по ноде на каждое ядро процессора.
Аналогичный файл создается и для клиента, приводить его не буду.
Образы
Сборка образов:
docker build -t server ./server
docker build -t client ./client
Параметр -t server
задает имя и тег образа в формате name:tag
.
Просмотр списка образов:
docker image ls
Удаление образов:
docker image rm <image_id>
При этом вводить полный id не обязательно, достаточно первых символов.
Отправка в репозиторий
Регистрируемся на Docker Hub и создаем репозиторий.
Предположим, я зарегистрировался там как myusername
и создал репозиторй public
. Пересоберем наши образы следующим образом:
docker build -t myusername/public:mean-docker-server ./server
docker build -t myusername/public:mean-docker-client ./client
Теперь можно отправить их:
docker push myusername/public:mean-docker-server
docker push myusername/public:mean-docker-client
Вытягивание образов
docker pull myusername/public:mean-docker-server
Остальные команды по работе с образами
docker image help
Контейнеры
Контейнер включает в себя:
- Docker-образ;
- среду исполнения;
- стандартный набор исполняемых команд.
Запуск приложения в контейнере
Для запуска приложений используем команды:
docker run -d -p 3000:3000 myusername/public:mean-docker-server
docker run -d -p 80:4000 myusername/public:mean-docker-client
Если образы локально не найдены, то они будут вытянуты из репозитория.
Образы наших приложений будут упакованы в контейнеры и запущены той командой, что мы указали в инструкции CMD
Dockerfile.
Параметром -d
мы указываем запустить контейнер в фоновом режиме.
Параметр -p
осуществляет проброс портов. Так наше клиентское приложение в контейнере доступно на порту 4000, а инструкцией -p
мы указываем привязать его на 80 порт хоста. Таким образом наше клиентское приложение будет доступно по адресу localhost:80
.
Просмотр списка контейнеров
Запущенные контейнеры:
docker container ls
docker ps
Все, в том числе остановленные:
docker container ls -a
docker ps -a
Остановка/запуск контейнеров
docker container pause <container_id>
docker container unpause <container_id>
docker container stop <container_id>
docker container start <container_id>
Выполнение команд в контейнере
docker exec <options> <container_id> <command>
Например, часто используется команда, которая позволяет подключиться к командной оболочке контейнера:
docker exec -it <container_id> sh
Логи
docker container logs <container_id>
Удаление контейнера
docker container rm <container_id>
Работающие контейнеры нужно либо сперва остановить, либо воспользоваться --force
:
docker container rm <container_id> -f
Другие команды для работы с контейнерами
docker container help
Очистка
Команда:
docker system prune
удалит:
- все остановленные контейнеры
- все неиспользуемые сети
- все промежуточные (dangling) образы
- весь кэш сборки
Для удаления всех неиспользуемых образов добавить параметр -a
.
В следующей статье читайте про работу с Docker Compose - инструментом для конфигурирования и запуска мультиконтейнерных приложений Docker.