MEAN-stack и Docker. Часть 1 - Dockerfile, образы, контейнеры

Ну вот и я добрался до Docker. До этого пытался, но все урывками, и цельная картина не складывалась. Но было принято решение, что свое приложение буду разворачивать в контейнерах. Поэтому сел раскуривать Docker основательно, параллельно конспектируя с целью написать статью. И вот, когда все детали сложились в общую картину, обнаружил, что наконспектировал 30+ страниц. Боюсь, что статья с таким объемом будет тяжела для восприятия, поэтому встречайте небольшой цикл, посвященный контейнеризации MEAN-stack приложений.

Для начала рекомендую пройти этот короткий официальный туториал Docker.

Содержание цикла

Описание демо-проекта

Демо-приложение не имеет особой ценности и предназначено только для того, чтобы показать работоспособность в целом. Оно должно принимать запросы, сохранять в базу данных своеобразный лог (имя хоста, req.hostname и x-real-ip заголовок запроса) и в ответе возвращать 10 последних сохраненных записей.

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

  • серверная часть
  • клиентская часть
  • база данных

В дальнейшем, по мере углубления, будут добавлены сервисы reverse proxy и мониторинга.

Код проекта на GitHub >>

Сервер

  • 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 >>

Кнопка, по нажатию на которую отправляется запрос на сервер, таблица со списком последних сохраненных в БД запросов:

alt

База данных

  • 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.

Часть 2 - Docker Compose >>

Павел Прудников

Постигающий дзен фулстэк вэб буддизма

Минск, Беларусь

Подписаться на Блог MEAN stack разработчика

Получайте свежие записи прямо на ваш почтовый ящик.

Или подпишитесь на RSS с Feedly!

Комментарии

comments powered by Disqus