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

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

d9euqzeswkvk_hjhbyiurb1nqmo.png

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


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

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

c48ba1c7cad01111368f0aae17bddb79.png


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

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


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

c48ba1c7cad01111368f0aae17bddb79.png


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

Теперь внесём в 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?

1ba550d25e8846ce8805de564da6aa63.png

© Habrahabr.ru