Всем привет! Рад вас снова видеть на мастер-классе по бэкенду!
На прошлой лекции мы узнали, как использовать Docker сеть,
чтобы связать два отдельных контейнера по именам. Сегодня я покажу вам, как
использовать docker-compose
для автоматической настройки всех сервисов
в одной и той же Docker сети и их одновременного запуска с помощью всего
лишь одной команды.
Хорошо, давайте начнём!
Во-первых, я создам новый файл: docker-compose.yaml
в корне нашего проекта.
Вы можете настроить множество параметров с помощью файла docker-compose
.
О всех них можно прочитать в документации, перейдя на docs.docker.com,
открыв Reference
, Compose file reference
, version 3
. В правом меню
вы можете увидеть список различных синтаксисов.
В этом видео я покажу вам некоторые из наиболее важных, которые мы будем
обычно использовать. Здесь на рисунке по центру страницы мы видим пример
compose
файла.
По сути вы определяете версию docker-compose
, а затем список сервисов,
которые вы хотите запустить вместе. В этом примере определяется сервис
redis
, сервис для БД postgres
и некоторые другие веб-сервисы для
голосования и вычисления его результатов.
Пока я просто скопирую первые две строки этого примера и вставлю их в наш
файл docker-compose.yaml
. Обратите внимание, что yaml
файл очень
чувствителен к отступам.
version: "3.9"
services:
На данный момент в файле используется отступ в 4 пробела, но я хочу использовать для них 2 пробела. Поэтому я нажму на кнопку, показанную на рисунке
выберу Indent using spaces
,
и 2 в качестве размера отступа.
Итак, если теперь мы нажмём Enter
, то увидим, что отступ для новых строк
равен 2 пробелам. Далее нам нужно объявить список сервисов, которые мы хотим
запустить. Первым сервисом должна быть БД postgres
. Для этого сервиса мы
будем использовать предварительно собранный Docker образ. Поэтому пишем
ключевое слово image
, за которым следуют «название» и «тег» образа, в
данном случае это postgres:12-alpine
. Теперь мы будем использовать ключевое
слово environment
, чтобы указать некоторые переменные окружения для имени
пользователя, пароля и названия базы данных, точно так же, как мы делали это в
рабочем процессе GitHub CI.
postgres:
image: postgres:12-alpine
environment:
Сначала скопируем переменную POSTGRES_USER
. Синтаксис немного отличается,
мы должны использовать оператор равенства для присвоения значения. Точно так
же я скопирую переменную POSTGRES_PASSWORD
и, наконец, переменную
POSTGRES_DB
из файла ci.yaml
.
environment:
- POSTGRES_USER=root
- POSTGRES_PASSWORD=secret
- POSTGRES_DB=simple_bank
Хорошо, затем мы объявим сервис api
, который будет обслуживать все запросы
к API нашего простого банковского приложения. Для этого сервиса мы должны
собрать его образ из Golang исходников, поэтому под ключевым словом build
указываем context
для создания образа:
api:
build:
context: .
Эта точка означает текущую корневой каталог. Затем мы используем ключевое
слово dockerfile
, чтобы сообщить docker-compose
, где найти Docker файл
для создания образа. В данном случае это просто Dockerfile в корне
проекта. Затем мы должны открыть доступ к порту 8080
извне, для хост-машины,
чтобы мы могли вызывать API нашего простого банковского приложения из-за
пределов контейнера.
api:
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:8080"
Одна из самых важных вещей, которые мы должны сделать, — это сообщить
сервису api
, как подключиться к сервису postgres
. Для этого мы установим
одну переменную среды: DB_SOURCE
. Как мы видели в предыдущей лекции,
установка этой переменной окружения переопределит значение, объявленное в
файле app.env
, и, поскольку все сервисы в этом файле docker-compose
будут
работать в одной сети, они могут взаимодействовать друг с другом по имени.
Поэтому здесь, в этом URL DB_SOURCE=postgresql://root:secret@localhost:5432/simple_bank?sslmode=disable
вместо localhost
мы будем использовать название сервиса postgres
.
api:
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:8080"
environment:
- DB_SOURCE=postgresql://root:secret@postgres:5432/simple_bank?sslmode=disable
И на этом в принципе всё! Файл docker-compose
готов. Попробуем его
запустить!
Если на вашем компьютере установлена последняя версия Docker CLI
, всё, что
вам нужно сделать, это запустить docker-compose up
docker-compose up
Затем Docker-compose автоматически найдет файл docker-compose.yaml
в
текущей папке и запустит его для вас. Как вы можете видеть здесь, перед
запуском сервиса он должен сначала создать Docker образ для сервиса api
нашего простого банковского приложения. Затем, после того, как образ будет
успешно собран, Docker-compose запустит сразу как сервис postgres
, так и
api
сервис.
В логах по префиксу строки видно, какой сервис его послал. В данном случае это
либо postgres_1
, либо api_1
. Теперь, если в другой вкладке терминала мы
запустим
docker images
то увидим новый образ simplebank_api
.
Как видно, имя образа имеет префикс simplebank
, что является названием
папки, содержащей наш Docker файл. А его суффикс — это название самого
сервиса, то есть api
.
Теперь давайте запустим
docker ps
чтобы увидеть все запущенные сервисы. Существует два сервиса:
simple_bank_postgres_1
и simple_bank_api_1
. Оба они имеют префикс
simple_bank
за которым идёт название сервиса.
Теперь, если мы вернёмся к окну терминала, где был запущен docker-compose
и прокрутим вверх, то увидим, что новая сеть simplebank_default
создаётся
до двух контейнеров с сервисами.
Теперь если мы проинспектируем сеть simplebank_default
, то увидим, что
два контейнера с сервисами на самом деле работают в этой же сети.
Вот почему они могут обнаруживать друг друга по именам.
Хорошо, теперь пришло время отправить несколько реальных запросов к API, чтобы увидеть, правильно ли работает сервис. Поскольку в новой базе данных ничего нет, я просто отправлю запрос к API для создания пользователя.
Ой, мы получили 500 Internal Server Error
и причина в следующем: "relation
users does not exist" ("отношения (таблицы) users не существует"). Знаете
почему?
Потому что мы еще не выполнили миграцию для создания схемы базы данных. Для
этого мы должны обновить наш Docker образ, чтобы запустить миграцию базы
данных перед запуском API сервера. Мы сделаем это по аналогии с тем, как мы
это делали в рабочем процессе GitHub CI: нам нужно будет загрузить двоичный
файл golang-migrate
в Docker образ и использовать его для запуска миграции.
Итак, я скопирую эти 2 инструкции и вставлю их в Dockerfile
на этапе сборки.
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.14.1/migrate.linux-amd64.tar.gz | tar xvz
sudo mv migrate.linux-amd64 /usr/bin/migrate
Сначала мы должны запустить эту curl
команду
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.14.1/migrate.linux-amd64.tar.gz | tar xvz
для загрузки и распаковки двоичного файла migrate
.
RUN curl -L https://github.com/golang-migrate/migrate/releases/download/v4.14.1/migrate.linux-amd64.tar.gz | tar xvz
Затем я перенесу эту вторую команду в этап запуска.
sudo mv migrate.linux-amd64 /usr/bin/migrate
где мы скопируем из builder
загруженный двоичный файл migrate
в
окончательный образ. Нам нужно изменить путь к исходному файлу migrate
на /app
, потому что это рабочий каталог на этапе сборки, куда мы загружаем
и распаковываем файл.
COPY --from=builder /app/migrate.linux-amd64 /usr/bin/migrate
Затем я помещу этот файл в ту же папку WORKDIR
на этапе запуска образа,
которой также является /app
.
COPY --from=builder /app/migrate.linux-amd64 ./migrate
Далее нам также нужно скопировать все файлы SQL миграций из папки
db/migration
в образ. Поэтому скопируйте db/migration
. Я помещу её
в папку migration
в текущем рабочем каталоге.
COPY db/migration ./migration
Ещё, что нам нужно сделать, так это установить curl
на этапе сборки образа,
потому что по умолчанию базовый образ Alpine не содержит
предустановленного curl
. Для этого нам просто нужно добавить туда
инструкцию RUN apk add curl
. Наконец, мы должны изменить способ запуска
приложения. Так, чтобы можно было выполнить миграцию базы данных перед
запуском двоичного файла main
. Я собираюсь создать новый файл: start.sh
в корне нашего проекта. Затем давайте изменим этот файл, чтобы сделать его
исполняемым.
chmod +x start.sh
Этот файл будет запускаться с помощью /bin/sh
, потому что мы используем
образ Alpine, а в нём оболочка bash
недоступна. Мы используем инструкцию
set -e
, чтобы гарантировать, что скрипт немедленно завершится, если команда
вернет ненулевой статус. Первый шаг, мы запустим миграцию БД. Итак, мы
запускаем двоичный файл /app/migrate
, передаём путь к папке, содержащей
все файлы SQL миграций, то есть /app/migration
. Затем URL-адрес базы
данных, который мы возьмем из переменной окружения DB_SOURCE
. Итак, здесь
я просто использую $DB_SOURCE
, чтобы получить его значение. Давайте также
воспользуемся опцией -versbose
, чтобы подробно выводились все детали при
запуске миграции. Наконец, аргумент up
используется для применения
всех миграций.
set -e
echo "run db migration"
/app/migrate -path /app/migration -database "$DB_SOURCE" -verbose up
После выполнения migrate up
мы запустим приложение. Все, что нам нужно
сделать на этом шаге, это вызвать exec "$@"
.
echo "start the app"
exec "$@"
По сути это означает: заменить "$@" на все параметры, переданные скрипту, и
запустить на выполнение. В нашем случае мы ожидаем, что передаваться
будет /app/main
, как определено в этой инструкции CMD
.
CMD ["/app/main"]
Чтобы это сработало, мы будем использовать инструкцию ENTRYPOINT
и укажем
файл /app/start.sh
в качестве основной точки входа Docker образа.
ENTRYPOINT ["/app/start.sh"]
Имейте в виду, что когда инструкция CMD
используется вместе с ENTRYPOINT
,
она будет вести себя так, как будто мы передали дополнительные параметры в
скрипт точки входа. То есть по сути, это будет похоже на запуск
"/app/starts.sh" с "/app/main" в качестве второго аргумента. Но, отделив
команду от точки входа, мы получаем большую гибкость и сможем заменить ее
другой командой во время выполнения, когда захотим. Подробнее об этом можно
прочитать на странице документации Docker, в разделе инструкций CMD
.
# Builds stage
FROM golang:1.16-alpine3.13 AS builder
WORKDIR /app
COPY . .
RUN go build -o main main.go
RUN apk add curl
RUN curl -L https://github.com/golang-migrate/migrate/releases/download/v4.14.1/migrate.linux-amd64.tar.gz | tar xvz
# Run stage
FROM alpine3.13
WORKDIR /app
COPY --from=builder /app/main .
COPY --from=builder /app/migrate.linux-amd64 /usr/bin/migrate
COPY app.env .
COPY db/migration ./migration
EXPOSE 8080
CMD ["/app/main"]
ENTRYPOINT ["/app/start.sh"]
Хорошо, теперь когда Docker файл обновлён, давайте попробуем снова запустить
docker-compose
. Но сначала нам нужно выполнить docker-compose down
, чтобы
удалить все существующие контейнеры и сети. И мы также должны удалить
образ simplebank_api
, потому что мы хотим пересобрать образ с учётом
новых скриптов для миграции БД.
docker rmi simplebank_api
Итак, после этой команды образ будет удалён. Давайте снова запустим
docker-compose up
! Для воссоздания образа потребуется некоторое время.
К сожалению, мы получили ошибку: "start.sh: no such file or directory" ("start.sh: нет такого файла или каталога").
Это потому что я забыл скопировать файл start.sh
в Docker образ. Итак,
давайте сделаем это прямо сейчас!
Здесь на этапе запуска нам просто нужно добавить еще одну инструкцию:
COPY start.sh .
Вот и все! Теперь ошибок быть не должно. Давайте запустим
docker-compose down
, чтобы удалить все контейнеры. Выполните
docker rmi simplebank_api
чтобы удалить старый образ simplebank_api
и наконец, запустите
docker-compose up
, чтобы всё воссоздать и перезапустить! На этот раз
похоже, что скрипт миграции базы данных был запущен, однако он похоже
завершился с ошибкой
Сервис завершил свою работу с кодом 1. И в логе мы видим ошибку: "connection
refused" ("в соединении отказано"). Приложение не может подключиться к базе
данных для выполнения миграции базы данных. Это связано с тем, что серверу
Postgres требуется некоторое время, чтобы он был готов принимать запросы, но
приложение попыталось запустить миграцию базы данных сразу после его запуска.
В тот момент сервер Postgres еще не был готов принять запрос на подключение.
Поэтому, чтобы исправить эту ошибку, мы должны указать приложению, что
нужно ждать готовности Postgres, прежде чем пытаться запустить скрипт
миграции БД и запустить API сервер. Даже несмотря на то, что в файле
docker-compose.yaml
мы можем использовать инструкции depends_on
,
чтобы сообщить docker-compose
, что сервис api
зависит от сервиса
postgres
, это только гарантирует, что postgres
будет запущен до сервиса
api
. Они не гарантируют, что postgres
готов принимать запросы перед
запуском сервиса api
.
Подробнее об этом поведении можно прочитать на странице документации
docker-compose
. Просто найдите инструкцию depends_on
на странице
https://docs.docker.com/compose/compose-file/compose-file-v3/.
Там вы увидите, что перечислено несколько особенностей, о которых нужно знать
при использовании инструкции depends_on
. Если мы хотим дождаться
готовности сервиса, мы должны перейти по этой ссылке,
чтобы узнать, как управлять порядком запуска. Существует несколько
инструментов, написанных для решения этой проблемы, но в нашем случае мы
должны воспользоваться sh-совместимым скриптом wait-for
, потому что мы
используем образ на основе Alpine.
Итак, вот Github страница скрипта.
Wait-for
предназначен для синхронизации таких сервисов, как Docker
контейнеры. И его использовать довольно просто. Нам просто нужно запустить
wait-for
и передать URL-адрес host:port
, готовность которого мы хотим
ожидать.
Вот пример,
$ ./wait-for www.eficode.com:80 -- echo "Eficode site is up"
Eficode site is up
По сути в нём ожидается, готовность веб-страницы eficode.com
на порту
80 перед выполнением оператора echo
. Итак, я собираюсь открыть последнюю
версию скрипта и нажать на эту ссылку, чтобы загрузить его (смотри рисунок).
Файл будет загружен в папку Downloads
. Поэтому я открою терминал и скопирую
этот файл в корень проекта нашего простого банковского приложения. Я
переименую его в wait-for.sh
, чтобы был более понятен типа файла.
mv ~/Downloads/wait-for ./wait-for.sh
Обратите внимание, что этот файл еще не является исполняемым, поэтому мы должны выполнить команду для изменения прав доступа, чтобы сделать его таким.
chmod +x wait-for.sh
Хорошо, теперь вернемся к нашему Dockerfile
. Я собираюсь добавить еще одну
инструкцию для копирования файла wait-for.sh
в окончательный Docker образ.
COPY wait-for.sh .
Затем в файле docker-compose
мы должны переопределить точку входа и
команду по умолчанию. Таким образом, он будет ждать готовности сервиса
postgres
, прежде чем пытаться запустить сервис api
. Давайте добавим сюда
entrypoint
, и ее содержимое должно быть: /app/wait-for.sh
. Затем хост и
порт, который мы ожидаем, должны быть postgres: 5432
, затем два тире, и,
наконец, скрипт start.sh
, запускаемый после того, как сервер postgres
будет готов.
version: "3.9"
services:
postgres:
image: postgres:12-alpine
environment:
- POSTGRES_USER=root
- POSTGRES_PASSWORD=secret
- POSTGRES_DB=simple_bank
api:
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:8080"
environment:
- DB_SOURCE=postgresql://root:secret@postgres:5432/simple_bank?sslmode=disable
depends_on:
- postgres
entrypoint: ["/app/wait.sh", "postgres:5432", "--", "/app/start.sh"]
Вы должны понимать, что когда ENTRYPOINT
переопределяется таким образом,
она также удаляет любую команду по умолчанию в образе, а это означает, что
инструкция CMD
, которую мы написали в Dockerfile
, будет проигнорирована.
Таким образом, в этом случае мы также должны явно указать команду, которую
мы хотим запустить, в файле docker-compose.yaml
. Для этой цели мы
используем ключевое слово command
. И команда, которую мы хотим запустить,
это просто: /app/main
.
entrypoint: [ "/app/wait.sh", "postgres:5432", "--", "/app/start.sh" ]
command: [ "/app/main" ]
И это всё что нужно сделать! Думаю, теперь у нас есть всё необходимое для
успешного запуска приложения. Давайте попробуем еще раз! Сначала выполним
docker-compose down
, чтобы удалить все существующие контейнеры. Запустите
docker rmi
, чтобы удалить образ simplebank_api
.
docker rmi simplebank_api
И, наконец, выполните
docker-compose up
чтобы пересобрать и перезапустить сервисы. На этот раз вы увидите, что сервисы запускаются по порядку.
Сначала будет запущена сервис postgres
, и только когда она будет готова
прослушивать соединения, сервис api
начнет выполнять миграцию базы данных.
Таким образом, миграции были выполнены успешно, сначала миграция
init_schema
, а затем миграция add_user
. Наконец, сервер API был успешно
запущен после миграции базы данных (смотри рисунок). Мы видим в логах Gin,
что сервер прослушивает и обслуживает HTTP-запросы на порту 8080.
Итак, давайте откроем Postman, чтобы протестировать API сервер! Я собираюсь отправить тот же запрос для создания пользователя, что и раньше.
Но на этот раз он выполнен успешно. Создан новый пользователь! Потрясяюще!
На этом я закончу сегодняшнюю лекцию о docker-compose
.
Теперь вы знаете, как использовать этот мощный инструмент для одновременного запуска нескольких сервисов, когда нужен определенный порядок запуска.
Большое спасибо за время, потраченное на чтение, желаю вам получать удовольствие от написания кода, и до встречи на следующей лекции!