Докеризация приложения, построенного на базе React, Express и MongoDB

от автора

Автор статьи, перевод которой мы публикуем сегодня, хочет рассказать о том, как упаковывать в контейнеры Docker веб-приложения, основанные на React, Express и MongoDB. Здесь будут рассмотрены особенности формирования структуры файлов и папок таких проектов, создание файлов Dockerfile и использование технологии Docker Compose.


Начало работы

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

Лучше всего, если клиентский и серверный код будет расположен в одной и той же папке. Код может располагаться в одном репозитории, но он может храниться и в разных репозиториях. В таком случае проекты стоит скомбинировать в одной папке с использованием команды git submodule. Я поступил именно так.

Дерево файлов родительского репозитория

React-приложение

Здесь я использовал проект, созданный с помощью Create React App и настроенный на поддержку TypeScript. Это — простой блог, содержащий несколько визуальных элементов.

Первым делом создадим файл Dockerfile в корневой директории client. Для того чтобы это сделать, достаточно выполнить такую команду:

$ touch Dockerfile 

Откроем файл и введём в него команды, представленные ниже. Как уже было сказано, я пользуюсь в своём приложении TypeScript, поэтому мне сначала нужно собрать его. Затем нужно взять то, что получилось, и развернуть это всё в формате статических ресурсов. Для того чтобы этого достичь, я пользуюсь двухступенчатым процессом сборки образа Docker.

Первый шаг работы заключается в использовании Node.js для сборки приложения. Я использую, в качестве базового образа, образ Alpine. Это — весьма компактный образ, что благотворно скажется на размере контейнера.

FROM node:12-alpine as builder WORKDIR /app COPY package.json /app/package.json RUN npm install --only=prod COPY . /app RUN npm run build 

Так начинается наш Dockerfile. Сначала идёт команда node:12-alpine as builder. Затем мы задаём рабочую директорию — в нашем случае это /app. Благодаря этому в контейнере будет создана новая папка. В эту папку контейнера копируем package.json и устанавливаем зависимости. Затем в /app мы копируем всё из папки /services/client. Работа завершается сборкой проекта.

Теперь надо организовать хостинг для только что созданной сборки. Для того чтобы это сделать, воспользуемся NGINX. И, опять же, это будет Alpine-версия системы. Делаем мы это, как и прежде, ради экономии места.

FROM nginx:1.16.0-alpine COPY --from=builder /app/build /usr/share/nginx/html EXPOSE 80 CMD ["nginx", "-g", "daemon off;"] 

Здесь в папку nginx копируются результаты сборки проекта, полученные на предыдущем шаге. Затем открываем порт 80. Именно на этом порте контейнер будет ожидать подключений. Последняя строка файла используется для запуска NGINX.

Это — всё, что нужно для докеризации клиентской части приложения. Итоговый Dockerfile будет выглядеть так:

FROM node:12-alpine as build WORKDIR /app COPY package.json /app/package.json RUN npm install --only=prod COPY . /app RUN npm run build FROM nginx:1.16.0-alpine COPY --from=build /app/build /usr/share/nginx/html EXPOSE 80 CMD ["nginx", "-g", "daemon off;"] 

Express-API

Наш Express-API тоже довольно прост. Тут, для организации конечных точек, используется технология RESTful. Конечные точки применяются для создания публикаций, для поддержки авторизации и для решения других задач. Начнём работу с создания Dockerfile в корневой директории api. Действовать будем так же, как и прежде.

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

FROM node:12-alpine as builder WORKDIR /app COPY package.json /app/package.json RUN apk --no-cache add --virtual builds-deps build-base python RUN npm install COPY . /app RUN npm run build 

Здесь всё очень похоже на тот Dockerfile, который мы использовали для клиентской части проекта, поэтому в подробности вдаваться мы не будем. Однако здесь есть одна особенность:

RUN apk --no-cache add --virtual builds-deps build-base python 

Я, перед сохранением паролей в базе данных, хэширую их с помощью bcrypt. Это — весьма популярный пакет, но при использовании его в образах, основанных на Alpine, наблюдаются некоторые проблемы. Тут можно столкнуться с примерно такими сообщениями об ошибках:

node-pre-gyp WARN Pre-built binaries not found for bcrypt@3.0.8 and node@12.16.1 (node-v72 ABI, musl) (falling back to source compile with node-gyp) npm ERR! Failed at the bcrypt@3.0.8 install script. 

Это — широко известная проблема. Её решение заключается в установке дополнительных пакетов и Python перед установкой npm-пакетов.

Следующий этап сборки образа, как и в случае с клиентом, заключается в том, чтобы взять то, что было сформировано на предыдущем этапе, и запустить это с помощью Node.js.

FROM node:12-alpine WORKDIR /app COPY --from=builder /app/dist /app COPY package.json /app/package.json RUN apk --no-cache add --virtual builds-deps build-base python RUN npm install --only=prod EXPOSE 8080  USER node CMD ["node", "index.js"] 

Здесь есть ещё одна особенность, которая заключается в установке только тех пакетов, которые предназначены для работы проекта в продакшне. Babel нам больше не нужен — ведь всё уже было скомпилировано на первом шаге сборки. Далее, мы открываем порт 8080, на котором серверная часть приложения будет ожидать поступления запросов, и запускаем Node.js.

Вот итоговый Dockerfile:

FROM node:12-alpine as builder WORKDIR /app COPY package.json /app/package.json RUN apk --no-cache add --virtual builds-deps build-base python RUN npm install COPY . /app RUN npm run build FROM node:12-alpine WORKDIR /app COPY --from=builder /app/dist /app COPY package.json /app/package.json RUN apk --no-cache add --virtual builds-deps build-base python RUN npm install --only=prod EXPOSE 8080  USER node CMD ["node", "index.js"] 

Docker Compose

Последний этап нашей работы заключается в объединении контейнеров api и client с контейнером, содержащим MongoDB. Для того чтобы это сделать, воспользуемся файлом docker-compose.yml, размещённым в корневой директории родительского репозитория. Это делается так из-за того, что из этого места есть доступ к файлам Dockerfile для клиентской и серверной частей проекта.

Создадим файл docker-compose.yml:

$ touch docker-compose.yml 

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

Итоговая структура файлов проекта

Теперь внесём в docker-compose.yml следующие команды:

version: "3" services:   api:     build: ./services/api     ports:       - "8080:8080"     depends_on:       - db     container_name: blog-api   client:     build: ./services/client     ports:       - "80:80"     container_name: blog-client   db:     image: mongo     ports:       - "27017:27017"     container_name: blog-db 

Тут всё устроено очень просто. У нас имеется три сервиса: client, api и db. Для MongoDB нет выделенного Dockerfile — Docker загрузит соответствующий образ со своего хаба и создаст из него контейнер. Это означает, что наша база данных будет пустой, но нас, для начала, это устроит.

В разделах api и client имеется ключ build, значение которого содержит путь к файлам Dockerfile соответствующих сервисов (к корневым директориям api и client). Порты контейнеров, назначенные в файлах Dockerfile, будут открыты в сети, организуемой Docker Compose. Это позволит приложениям взаимодействовать. При настройке сервиса api, кроме того, используется ключ depends_on. Он сообщает Docker о том, что, прежде чем запускать этот сервис, нужно дождаться полного запуска контейнера db. Благодаря этому мы сможем предотвратить возникновение ошибок в контейнере api.

И — вот ещё одна мелочь, имеющая отношение к MongoDB. В кодовой базе бэкенда нужно обновить строку подключения к базе данных. Обычно она указывает на localhost:

mongodb://localhost:27017/blog 

Но, применяя технологию Docker Compose, мы должны сделать так, чтобы она указывала бы на имя контейнера:

mongodb://blog-db:27017/blog 

Финальный шаг нашей работы заключается в том, чтобы всё это запустить, выполнив в корневой папке проекта (там, где находится файл docker-compose.yml) следующую команду:

$ docker-compose up 

Итоги

Мы рассмотрели несложную методику контейнеризации приложений, основанных на React, Node.js и MongoDB. Полагаем, если вам это понадобится, вы сможете легко адаптировать её для своих проектов.

P.S. Мы запустили маркетплейс на сайте RUVDS. Имеющийся там образ Docker устанавливается в один клик. Проверить работу контейнеров можно на VPS. Новым клиентам бесплатно предоставляются 3 дня для тестов.

Уважаемые читатели! Пользуетесь ли вы Docker Compose?

ссылка на оригинал статьи https://habr.com/ru/company/ruvds/blog/491710/