[Перевод] Докеризация приложения, построенного на базе 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?