Docker: заметки веб-разработчика. Итерация четвертая

от автора

Привет, друзья!

В этой статье я продолжаю (и заканчиваю) делиться с вами заметками о Docker.

Заметки состоят из 4 частей: 2 теоретических и 2 практических.

Если быть более конкретным:

  • первая часть посвящена Docker, Docker CLI и Dockerfile;
  • во второй части рассказывается о Docker Compose;
  • в третьей части мы разрабатываем приложение, состоящее из трех сервисов: клиента на React, админки на Vue и сервера на Express, и базы данных PostgreSQL, взаимодействие с которой осуществляется с помощью Prisma.

В этой заключительной части мы «контейнеризуем» наше приложение.

Репозиторий с кодом приложения.

Если вам это интересно, прошу под кат.

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

Клонировать и запустить приложение, с которым мы будем работать, можно следующим образом:

git clone https://github.com/harryheman/docker-test.git  cd docker-test  cd client yarn # or npm i  cd ../admin yarn  cd ../api yarn  cd .. yarn yarn dev # or npm run dev

Если вы используете npm, команды для запуска серверов для разработки в файле package.json должны выглядеть так:

"scripts": {   "dev:client": "npm run start --prefix services/client",   "dev:admin": "npm run dev --prefix services/admin",   "dev:api": "npm run dev --prefix services/api",   "dev": "concurrently \"npm run dev:client\" \"npm run dev:admin\" \"npm run dev:api\"" }

Dockerfile

Начнем с определения Dockerfile для сервисов нашего приложения.

В директории client создаем файл Dockerfile следующего содержания:

# дефолтная версия `Node.js` ARG NODE_VERSION=16.13.1  # используемый образ FROM node:$NODE_VERSION  # рабочая директория WORKDIR /client  # копируем указанные файлы в рабочую директорию COPY package.json yarn.lock ./  # устанавливаем зависимости RUN yarn  # копируем остальные файлы COPY . .  # выполняем сборку приложения RUN yarn build

Обратите внимание: на данном этапе вместо сборки (RUN yarn build) мы могли бы выполнять команду start для запуска сервера для разработки: CMD ["yarn", "start"], но если мы так сделаем, то впоследствии нам придется создавать отдельный Dockerfile для продакшна. Проще сразу определить производственную версию Dockerfile, а команду start запускать из docker-compose.yml.

Создаем практический идентичный Dockerfile в директории admin:

ARG NODE_VERSION=16.13.1  FROM node:$NODE_VERSION as build  WORKDIR /admin  COPY package.json yarn.lock ./  RUN yarn  COPY . .  RUN yarn build

Обратите внимание: сборка клиента будет находится в директории client/build, а сборка админки — в директории admin/dist. В файле api/index.js можно найти такие строки:

if (process.env.ENV === 'production') {   const clientBuildPath = join(__dirname, 'client', 'build')   const adminDistPath = join(__dirname, 'admin', 'dist')    app.use(express.static(clientBuildPath))   app.use(express.static(adminDistPath))   app.use('/admin', (req, res) => {     res.sendFile(join(adminDistPath, decodeURIComponent(req.url)))   }) }

Эти строки говорят нам о том, что при запуске сервера в производственном режиме (process.env.ENV === 'production'), он будет обслуживать статические файлы из названных выше директорий: клиент будет доступен по маршруту (роуту) /, а админка — по роуту /admin. Мы вернемся к этому позже.

Создаем похожий Dockerfile в директории api:

ARG NODE_VERSION=16.13.1  FROM node:$NODE_VERSION  WORKDIR /app  COPY package.json yarn.lock ./  RUN yarn  COPY . .  # выставляем порт EXPOSE 5000  # запускаем сервер в производственном режиме CMD ["yarn", "start"]

Обратите внимание: инструкции EXPOSE 5000 и CMD ["yarn", "start"] на данном этапе можно опустить, но они потребуются нам в продакшне. На самом деле, нам потребуется кое-что еще, но позвольте пока сохранить интригу.

Также обратите внимание, что я внес парочку изменений в проект:

  1. Содержание файла .env, находящегося корневой директории проекта:

    # добавил название приложения APP_NAME=my-app  # уточнил версию `Node.js` NODE_VERSION=16.13.1  POSTGRES_VERSION=14 POSTGRES_USER=postgres POSTGRES_PASSWORD=postgres POSTGRES_DB=mydb  # перенес сюда путь к БД из файла `api/.env` # обратите внимание, что вместо `localhost` после символа `@` мы указываем название контейнера -  `postgres` DATABASE_URL=postgresql://postgres:postgres@postgres:5432/mydb?schema=public  ENV=development

  2. Команда для запуска сервера для разработки (файл api/package.json, раздел scripts):

    "dev": "prisma migrate dev && prisma db seed && nodemon",

Хорошей практикой считается исключение файлов из образа с помощью .dockerignore:

node_modules yarn-error.log # mac .DS_Store

Такой файл нужно создать в каждом сервисе.

После создания Dockerfile для каждого сервиса мы готовы к «контейнеризации» приложения.

Docker Compose

Создаем в корневой директории файл docker-compose.dev.yml следующего содержания:

# версия `compose` version: '3.9' # сервисы services:   # БД   postgres:     # файл, содержащий переменные среды окружения     env_file: .env     # название контейнера     container_name: ${APP_NAME}_postgres     # используемый образ     image: postgres:${POSTGRES_VERSION}     # именованный том для хранения данных     volumes:       - data_postgres:/var/lib/postgresql/data     # порт для доступа к БД     ports:       - 5432:5432     # политика перезапуска контейнера     restart: on-failure    client:     env_file: .env     container_name: ${APP_NAME}_client     image: node:${NODE_VERSION}     # рабочая директория     working_dir: /app     # анонимный том     # `rw` означает `read/write` - чтение/запись     volumes:       - ./services/client:/app:rw     # сервис, от которого зависит работоспособность данного сервиса     depends_on:       - api     ports:       - 3000:3000     restart: on-failure     # команда для запуска сервера для разработки     command: bash -c "yarn start"    admin:     env_file: .env     container_name: ${APP_NAME}_admin     image: node:${NODE_VERSION}     working_dir: /app     volumes:       - ./services/admin:/app:rw     depends_on:       - api     ports:       - 4000:4000     restart: on-failure     command: bash -c "yarn dev"    api:     env_file: .env     container_name: ${APP_NAME}_api     # ссылка на `Dockerfile`, на основе которого выполняется сборка     build: services/api     ports:       - 5000:5000     depends_on:       - postgres     restart: on-failure     # перезапись команды `yarn start`, определенной в `Dockerfile`     command: bash -c "yarn dev"  # тома volumes:   data_postgres:

Определим в package.json несколько команд для управления compose:

"dev:compose:up": "docker compose -f docker-compose.dev.yml up -d", "dev:compose:stop": "docker compose -f docker-compose.dev.yml stop", "dev:compose:rm": "docker compose -f docker-compose.dev.yml rm", "compose:up": "docker compose up -d", "compose:stop": "docker compose stop", "compose:rm": "docker compose rm"

Команда compose:up поднимает, команда compose:stop — останавливает, а команда compose:rm — удаляет сервис. Префикс dev: означает что поднимается/останавливается/удаляется сервис для разработки. В свою очередь, отсутствие данного префикса означает управление производственным сервисом (по умолчанию compose использует файл docker-compose.yml, которым мы займемся позже).

Еще несколько команд, которые могут пригодится при работе с compose при отладке приложения:

# список запущенных контейнеров docker ps  # список запущенных сервисов docker compose ls  # список образов docker images  # удаление образа # [image-name] - название образа docker image rm [image-name] # например docker image rm docker-test_api  # список томов docker volume ls  # удаление тома # [volume-name] - название тома docker volume rm [volume-name] # например docker volume rm postgres_data  # очистка системы (тома не удаляются) docker system prune -a

Поднимаем сервис в режиме для разработки с помощью команды yarn dev:compose:up или npm run dev:compose:up:

После создания контейнеров сервисам потребуется какое-то время на запуск, после чего они будут доступны по следующим адресам:

  • клиент: localhost:3000;
  • админка: localhost:4000;
  • сервер: localhost:5000 (нет прямого доступа; доступен для клиента и админки);
  • БД: postgres:5432 (нет прямого доступа; доступен только для сервера).

По сути, команда dev:compose:up делает тоже самое, что и команда dev + скрипт из файла db.

Чем производственный сервис будет отличаться от сервиса для разработки? Предположим, что мы хотим, чтобы всю статику приложения обслуживал сервер, поэтому нам требуется какой-то способ передать api сборки клиента и админки. Существует несколько способов это сделать. Одним из самых простых и удобных является использование Docker Hub.

Переходим по ссылке и создаем аккаунт.

Переходим в директорию client и создаем образ с тегом:

cd client # [username] - ваш логин для входа в dockerhub # тег образа обязательно должен начинаться с вашего логина docker build . -t [username]/docker-test_client # мой логин - aio350 docker build . -t aio350/docker-test_client

Авторизуемся в dockerhub и отправляем образ в свой реестр:

docker login  docker push aio350/docker-test_client

Делаем тоже самое для админки:

cd admin  docker build . -t aio350/docker-test_admin  docker push aio350/docker-test_admin

После этого в своем реестре dockerhub мы увидим следующую картину:

Немного отредактируем файл api/Dockerfile:

ARG NODE_VERSION=16.13.1  # копируем образ клиента из `dockerhub` # `AS` позволяет ссылаться на этот слой в других инструкциях FROM aio350/docker-test_client AS client # образ админки FROM aio350/docker-test_admin AS admin  FROM node:$NODE_VERSION  WORKDIR /app  COPY package.json yarn.lock ./  RUN yarn  COPY . .  # копируем сборку клиента COPY --from=client /client/build /app/client/build # копируем сборку админки COPY --from=admin /admin/dist /app/admin/dist  EXPOSE 5000  CMD ["yarn", "start"]

Создаем в корневой директории проекта файл docker-compose.yml следующего содержания:

version: '3.9' services:   postgres:     env_file: .env     container_name: ${APP_NAME}_postgres     image: postgres:${POSTGRES_VERSION}     volumes:       - data_postgres:/var/lib/postgresql/data     ports:       - 5432:5432     restart: on-failure   # статика нашего приложения обслуживается сервером   # поэтому нам не нужно поднимать сервисы `client` и `admin`   api:     env_file: .env     # перезаписываем переменную `ENV`, определенную в файле `.env`     environment:       - ENV=production     container_name: ${APP_NAME}_api     build: services/api     depends_on:       - postgres     ports:       - 5000:5000     restart: on-failure     # выполняется команда `yarn start`, определенная в `Dockerfile`  volumes:   data_postgres:

Удаляем сервис для разработки, удаляем образ docker-test_api, удаляем том docker-test_data_postgres и поднимаем производственный сервис:

yarn dev:compose:stop yarn dev:compose:rm  # для чистоты эксперимента docker image rm docker-test_api  docker volume rm docker-test_data_postgres  yarn compose:up

Теперь наш сервис состоит всего из 2 контейнеров.

Клиент доступен по адресу: localhost:5000, а админка — по адресу localhost:5000/admin.

Приложение работает, как ожидается.

На этом «контейнеризацию» нашего приложения можно считать завершенной.

Что касается настройки CI/CD, такого как GitLab CI/CD или GitHub Actions, то, пожалуй, это тема для отдельной статьи.

Пожалуй, это все, что я хотел рассказать вам о Docker.

Благодарю за внимание и happy coding!



ссылка на оригинал статьи https://habr.com/ru/articles/657361/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *