Всем привет! В данной статье попробую рассказать как написать docker-compose.yml для контейнеризации простого приложения, которое будет состоять из фронта на React, бэкенда на Spring Boot, также будем использовать базу данных PostgreSQL, а для просмотра данных в базе pgAdmin.  Данная статья будет полезна начинающим разработчиком разобраться для чего нужен docker-compose.yml файл и как начать с ним работать. Для понимания данной статьи необходимы минимальные знания по докеру, а также для использования примеров необходим установленный Docker у вас на компьютере. План, что мы хотим сделать: написать бэкенд с использованием Spring Boot (это будет простое приложение по работе с книгами, которое будет выполнять CRUD операции над ними); подключить базу данных PostgreSQL для хранения данных; подключить pgAdmin для просмотра данных в нашей базе PostgreSQL; написать простой фронт на React для отображения, ввода, редактирования и      удаления книг; поместить все это в контейнеры и "заставить работать", для чего и будем использовать docker-compose.yml файл. Начнем немного с теории. Docker-compose используется для управления несколькими контейнерами, входящими в состав одного приложения, это могут быть контейнеры созданные на основе образов нашего приложения (у нас это будут два контейнеры: один с бэкендом, а второй с фронтендом), а также сторонние контейнеры скачанные, например, с docker hub (у нас это будет контейнеры с базой данных и графическим клиентом к ней). И конечно же логичный вопрос: для чего все это? зачем? Ответ очень прост, Docker compose нужен для быстрого развертывания приложения, например, перенос приложения на другой сервер займет несколько минут, также в сочетании с kubernetes - дает превосходные результаты по автоматизации развертывания, масштабирования и координации работы нашего приложения в условиях кластера. Итак приступим. Вначале напишем наш бэкенд, для этого идет на https://start.spring.io/ и создаем проект. Подробно не буду описывать каждый шаг разработки, буду останавливаться только на самых главных моментах, в любом случае, весь код проекта будет доступен на гитхабе, ссылка в конце статьи. Открываем наш проект в среде разработки и вначале немного модифицируем наш pom.xml файл. Из основных зависимостей: postgresql - доступ к базе данных; liquibase - будет использоваться для "накатывания" таблиц в базе данных и заполнения первоначальными данными. mapstruct - для маппинга наших сущностей. lombok - для сокращения шаблонного кода через использование аннотаций. Начнем с кода нашей сущности, как я упоминал раньше - это будет книга (Book). Поэтому создадим класс Book. Далее интерфейс BookRepository и унаследуем его от JpaRepository - чтобы получить набор стандартных методов JPA для работы с БД. Создадим класс BookDto, так как уровень сервиса не должен напрямую работать с нашей сущностью Book (может в данном конкретном случае это и не нужно, так как это немного усложняет код). Создадим интерфейс BookMapper для маппинга наших сущностей из Book и BookDto и обратно, здесь мы и используем mapstruct. Создадим еще один интерфейс BookService, в котором и напишем основные методы работы с нашими книгами. и реализуем эти методы в классе BookServiceImpl. Последний класс, который мы создадим здесь - это будет BooksController - который и будет выдавать наши эндпойнты для просмотра, добавления, редактирования и удаления наших книг. Сейчас займемся нашим application.properties файлом и пропишем там доступ к нашей базе данных, включим настройки liquibase, настроим порт 8181 - чтобы наш бэкенд поднимался именно на этом порту, а также включим другие настройки. Осталось написать liquibase скрипты для накатывания таблицы базы данных и вставки первоначальной информации. Вначале создадим структуру папок как на рисунке То есть в папке resources создаем папку db и в ней папку changelog, в которой находится файл db.changelog-master.xml, путь к нему прописан в нашем файле application.properties. Это будет основной файл, который будет запускать скрипты.  Далее в папке changelog создаем папку v.1.0.0, где создаем три файла. 2023-05-12-1-create-table-book.xml - накатываем таблицу book файл 2023-05-12-2-insert-books.xml - вставляем 6 книг в нашу таблицу и файл cumulative.xml, где мы аккумулируем все наши файлы со скриптами, которые хотим запустить. Еще напишем небольшой скрипт, который нам понадобится позже. К нему будет обращаться база данных postgreSQL и создавать там саму базу данных, к которой мы и будем подключаться. Для этого создадим папку infrastructure, а в ней папку db. Должно получиться так. И уже в папке db создадим файл create_db.sql - здесь мы создаем базу данных books_db. Сейчас в корне нашего проекта создаем Dockerfile Dockerfile нужен для того чтобы на основании его мы создали образ, а уже на основании образа запустили контейнер с нашим бэкендом. Здесь мы используем многоэтапную сборку, на первом этапе мы получаем jar-ник нашего проекта, а на втором мы его уже запускаем. Пройдемся по Dockerfile  FROM maven:3.8.4-openjdk-17 as builder - указываем на основании какого образа, который докер стянет с docker hub мы будем билдить наш проект, as builder - это название, которое мы присвоили, для того чтобы обратиться с другого слоя образа для получения данных. WORKDIR /app - создаем директорию app внутри слоя образа. COPY . /app/. - копируем все наши папки с текущего проекта в папку app в слое образа. RUN mvn -f /app/pom.xml clean package -Dmaven.test.skip=true   - запускаем maven, который билдит наш проект и получаем jar-ник. FROM eclipse-temurin:17-jre-alpine - снова указываем на основании какого образа, мы будем запускать наш проект, здесь уже мы не используем jdk, а только jre - так как нам не нужны инструменты разработчика. WORKDIR /app - создаем директорию app в новом слое образа. COPY --from=builder /app/target/.jar /app/.jar - копируем с предыдущего слоя с папки target наш jar-ник в папку app. EXPOSE 8181 - указываем на каком порту должен работать наш контейнер. ENTRYPOINT ["java", "-jar", "/app/*.jar"] - запускаем наше приложение в контейнере. Пришло время написать сам docker-compose.yml файл. Снова в корне нашего проекта создаем файл docker-compose.yml. Пройдемся немного по Dockerfile. version: '3.8' - указываем версию docker compose services: - указываем какие контейнеры нам нужно будет поднять. client-backend: - название первого контейнера с нашим бэкендом. image: client:0.0.1 - указываем название контейнера и его тэг (можно без тэга, тогда будет автоматически присвоен latest). build: - указываем, что мы хотим его получить не скачивая с докер хаба, а будем "строить" на основании Dockerfile. context: . - указываем расположение Dockerfile, он у нас располагается в той же папке, что и docker-compose.yml dockerfile: Dockerfile - указываем наименование Dockerfile здесь указываем первый порт - это внешний порт, с помощью которого мы можем получить доступ к нашему контейнеру, а второй порт - это внутренний порт контейнера. указываем, что наш контейнер должен подняться после того как поднимется контейнер с базой данных, так как, если контейнер с бэкендом поднимется первым и не будет доступа к базе данных, то он упадет с ошибкой. указываем дополнительные настройки, в том числе url для подключения к базе данных. В этом пути мы уже используем внутреннее имя нашего контейнера с базой данных service-db. указываем имя нашего контейнера с базой данных, а также имя образа, который будет скачен с докер хаба. Желательно при этом указывать тэг образа, то есть версию, если версия не будет указана, то будет скачена и использована самая последняя версия данного образа, что может привести в будущем к несовместимости по версиям и ошибкам. указываем username и password, они должны совпадать с теми, что мы указали в application.properties. указываем порты. здесь мы указываем, чтобы выполнился наш скрипт, который мы написали ранее и который расположен в папке infrastructure/db по созданию базы данных books_db. а во втором volume мы делаем чтобы наши данные сохранялись не локально в наш контейнер, а в файл, расположенный вне контейнера. Это нужно для того чтобы при перезапуске образа, если мы его удалим и поднимем снова, чтобы наши изменения в базе данных не потерялись, а сохранились. Это я продемонстрирую чуть позже. С контейнером pgadmin вроде все понятно, остановлюсь только на это данные для входа в pgadmin по умолчанию. Написали много кода, теперь его надо протестить, вначале протестим его без фронта, но потом еще добавил и его. Итак, переходим в терминал, в папку с проектом и выполняем команду docker-compose up После выполнения данной команды, необходимо будет подождать несколько минут пока докер сбилдит наш образ, а также стянет все образы с докер хаба. В конце, в консоли вы должны будете увидеть запуск нашего бэкенда. И если мы зайдем в Docker Desktop, то должны увидеть все три наши работающие контейнеры. Давайте протестим как работает наш бэкенд. Для этого через Postman выполним команду http://localhost:8181/api/v1/books и мы получим 6 наших книг, которые мы записали с помощью liquibase. Зайдем в браузере на http://localhost:5050 и введем username: admin@admin.com и пароль: root Мы должны зайти на pgAdmin Здесь мы должны создать подключение к нашей базе данных. Для этого нажимаем Add New Server. Name задаем любое.  Во вкладке Connection указываем Host name - наименование, которое мы указали для контейнера с нашей БД (service-db). Port - внутренний порт контейнера БД (5432). Maintenance database - наименование нашей базы данных (books_db). Username - тот юзернайм, который мы указали в docker-compose.yml для подключения к БД (username). Password- тот пароль, который мы указали в docker-compose.yml для подключения к БД (password). Далее если мы найдем нашу таблицу book и выполним в ней SELECT * FROM book мы увидим 6 наших книг. Давайте добавим еще одну книгу. Для этого идем в Postman и отправим POST запрос на адрес http://localhost:8181/api/v1/book с JSON книги, которую мы хотим добавить. Идем обратно в pgAdmin и смотрим какие книги у нас есть. Последняя книга также появилась. Сейчас остановим все наши контейнеры и удалим их, это можно сделать или через Docker Desktop или командами в терминале. Я это сделаю через Docker Desktop. И снова их поднимем с помощью команды docker-compose up. И зайдя снова на pgAdmin мы увидим 7 нашим книг, в том числе с последней, которую мы добавили. Сейчас осталась последняя часть нашей работы - написать фронтэнд на React. Для этого у вас на компьютере должен быть установлен Node.js и npm. Чтобы проверить какие версии у вас установлены или вообще есть ли у вас Node.js и npm, можно воспользоваться командами node --version и npm --version. Переходим в терминал и в корне нашего проекта выполняем команду npx create-react-app@5 frontend Данной командой мы создадим React проект в папке frontend (папка появиться автоматически) в нашем основном проекте. Командой cd frontend зайдем в папку frontend. И выполним команду npm i bootstrap@5 react-cookie@4 react-router-dom@6 reactstrap@9  , данной командой мы установим Bootstrap , поддержку файлов cookie для React, React Router и Reactstrap. Если открыть папку frontend, то мы увидим следующую структуру папок.  Данный проект c фронтом можно делать и отдельно в другой директории (не в проекте с бэкендом), тогда надо будет поменять путь к Dockerfile к фронту в docker-compose.yml В файл index.js добавим следующий импорт import 'bootstrap/dist/css/bootstrap.min.css';  Также в файл package.json добавим строку "proxy": "http://host.docker.internal:8181" - для общения нашего фронта с бэкендом на порту 8181 через контейнер.   Добавим файл BookEdit.js для редактирования книг. Добавим также файл BookList.js для отображения книг. Файл App.js исправим следующим содержанием. Добавим файл Home.js - это будет наша стартовая страница. Добавим файл AppNavbar.js Наш фронт готов. Теперь напишем Dockerfile для того, чтобы сделать на его основе образ. В корне папки frontend создаем файл Dockerfile следующего содержания. FROM node:18-alpine - указываем на основании какого образа мы будем запускать наше приложение с фронтом. У нас будет образ внутри которого будет node и npm. WORKDIR /app - как всегда внутри образа создаем папку app, в которой мы и будем сохранять наше приложение. EXPOSE 3000 - на каком порту будет работать наш фронт. COPY ["package.json", "package-lock.json*", "./"] - копируем файлы package.json и package-lock.json в наш образ, содержащие зависимости. RUN npm install -  команда, устанавливающая пакеты, то есть она скачает пакет в папку проекта node_modules в соответствии с конфигурацией в файле package.json, обновив версию пакета везде, где это возможно (и, в свою очередь, обновив package-lock.json)  COPY . . - копируем исходный код в образ. CMD ["npm", "start"] - запускаем проект. Осталось добавить в наш docker-copmose.yml файл, информацию чтобы он автоматически создавал образ с нашим фронтом. Теперь docker-compose будет выглядеть так. Мы добавили новый сервис - то есть новый образ на основании которого запустится контейнер. здесь мы указали, что хотим чтобы docker сбилдил контейнер на основании образа, полученного на основании Dockerfile расположенного в папке frontend. запускаем наш фронт на порту 3000. Файл docker-compose полностью готов. Запускаем. Снова входим в терминал в папку с нашим проектом и запускаем команду docker-compose up Все наши контейнеры должны работать. Переходим в браузер и заходим на адрес http://localhost:3000/ Нажимаем на Manage My Books и мы должны получить все наши книги. Также протестируем добавление книги. Удаление книги. И изменение книги Все работает. Также можете убедиться, что все изменения переносятся в нашу базу данных. Вот и мы подошли к концу с написанием нашего небольшого приложения по сохранению книг и использовали при этом docker-compose файла. Данная статья получилась очень большой. Поэтому особенно всем СПАСИБО, кто дочитал до конца. Всем пока!!! P.S. Вот ссылки на проект на гитхабе: бэкенд и фронтэнд.