Начинаем работать с Docker Compose

  • Михаил
  • 12 мин. на прочтение
  • 131
  • 17 Feb 2020
  • 17 Feb 2020

Инструмент Docker Compose входит в комплект официального программного обеспечения для Docker. Он позволяет решать различные задачи, связанные с управлением одновременно несколькими контейнерами, составляющих в целом один проект.

Возможности Docker Compose

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

Самый очевидный пример – веб-сайт, где для авторизации пользователей необходимо подключение к базе данных. Для такого проекта нужно два сервиса – один отвечает за функционирование сайта, а другой за базу данных. Соответственно, разработчику понадобится средство, позволяющее управлять одновременно двумя контейнерами. И тут на помощь приходит Docker Compose, который позволяет включать два и более сервиса всего одной командой.

В этом и заключается разница между Docker и Docker Compose. Первый используется для работы с контейнерами по отдельности. Второй позволяет одновременно управлять несколькими контейнерами, а следовательно, работать с более сложными проектами.

Практика применения

Практику использования Docker Compose можно рассмотреть на примере веб-проекта, состоящего из двух сайтов. Первый позволяет создать интернет-магазин буквально за несколько кликов мышью. Второй служит для поддержки клиентов. Оба сайта подключены к общей базе данных.

Допустим, по мере развития проекта становится понятно, что мощностей текущего сервера уже недостаточно. Принимается решение перенести сайты на новый сервер. Без Docker Compose придется переносить и настраивать все сервисы заново. Тогда сама работа займет много времени. Кто же возникает вероятность что-нибудь забыть в процессе.

При помощи Docker Compose можно выполнить перенос сайтов при помощи всего нескольких команд. Потребуется лишь изменить некоторые настройки и перенести на другой сервер резервную копию баз данных.

На предыдущих этапах было создано два проекта – серверное и клиент-приложение. Оба они обладают своим файлом Dockerfile. До этого момента применялись только основной функционал Docker. Далее к работе будет подключен Docker Compose — потребуется отредактировать docker-compose.yml, созданный ранее.

В этом примере не стоит задача ознакомиться со всеми возможными командами, доступными для использования в docker-compose.yml. Основная цель – увидеть, как Docker Compose можно применить на практике.

При интенсивном использовании Docker управление несколькими разными контейнерами быстро становится громоздким. Docker Compose — это инструмент, который помогает нам преодолеть эту проблему и легко обрабатывать несколько контейнеров одновременно. 

 

Объяснение конфигурации YAML

Короче говоря, Docker Compose работает, применяя множество правил, объявленных в одном конфигурационном файле docker-compose.yml .

Эти правила YAML , как удобочитаемые, так и оптимизированные для машин, обеспечивают эффективный способ сделать снимок всего проекта с десяти тысяч футов в несколько строк.

Почти каждое правило заменяет определенную команду Docker, так что в итоге нам достаточно выполнить:

docker-compose up

Мы можем получить десятки конфигураций, применяемых Compose под капотом. Это избавит нас от необходимости писать сценарии с помощью Bash или чего-то еще.

В этом файле нам нужно указать версию формата файла Compose, как минимум один сервис и опционально тома и сети :

version: "3.7"
services:
  ...
volumes:
  ...
networks:
  ...

Давайте посмотрим, что это за элементы на самом деле.

 

Услуги

В первую очередь сервисы относятся к конфигурации контейнеров .

 

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

services:
  frontend:
    image: my-vue-app
    ...
  backend:
    image: my-springboot-app
    ...
  db:
    image: postgres
    ...

Есть несколько настроек, которые мы можем применить к службам, и мы рассмотрим их более подробно позже.

 

Объемы и сети

С другой стороны, тома — это физические области дискового пространства, разделяемые между хостом и контейнером или даже между контейнерами. Другими словами, том — это общий каталог на хосте , видимый из некоторых или всех контейнеров.

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

Опять же, мы узнаем о них больше в следующем разделе.

 

Анализ сервиса

Теперь приступим к осмотру основных настроек сервиса.

 

Вытягивание изображения

Иногда образ, который нам нужен для нашего сервиса, уже был опубликован (нами или кем-то еще) в Docker Hub или другом Docker Registry.

Если это так, то мы обращаемся к нему с помощью атрибута изображения , указав имя изображения и тег:

services: 
  my-service:
    image: ubuntu:latest
    ...

 

Создание образа

В качестве альтернативы нам может понадобиться создать образ из исходного кода, прочитав его Dockerfile .

На этот раз мы будем использовать ключевое слово build , передав путь к Dockerfile в качестве значения:

services: 
  my-custom-app:
    build: /path/to/dockerfile/
    ...

Мы также можем использовать URL вместо пути:

services: 
  my-custom-app:
    build: https://github.com/my-company/my-project.git
    ...

Кроме того, мы можем указать имя образа в сочетании с атрибутом сборки , который будет называть изображение после создания, делая его доступным для использования другими службами :

services: 
  my-custom-app:
    build: https://github.com/my-company/my-project.git
    image: my-project-image
    ...

 

Настройка сети

Контейнеры Docker взаимодействуют между собой в сетях, созданных неявно или посредством конфигурации с помощью Docker Compose . Служба может взаимодействовать с другой службой в той же сети, просто ссылаясь на нее по имени контейнера и порту (например, network-example-service:80 ), при условии, что мы сделали порт доступным через ключевое слово expose :

services:
  network-example-service:
    image: karthequian/helloworld:latest
    expose:
      - "80"

В этом случае он также будет работать без его раскрытия, потому что директива expose уже находится в образе Dockerfile .

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

services:
  network-example-service:
    image: karthequian/helloworld:latest
    ports:
      - "80:80"
    ...
  my-custom-app:
    image: myapp:latest
    ports:
      - "8080:3000"
    ...
  my-custom-app-replica:
    image: myapp:latest
    ports:
      - "8081:3000"
    ...

Порт 80 теперь будет виден с хоста, а порт 3000 двух других контейнеров будет доступен через порты 8080 и 8081 на хосте. Этот мощный механизм позволяет запускать разные контейнеры, открывающие одни и те же порты, без конфликтов .

Наконец, мы можем определить дополнительные виртуальные сети для разделения наших контейнеров:

services:
  network-example-service:
    image: karthequian/helloworld:latest
    networks: 
      - my-shared-network
    ...
  another-service-in-the-same-network:
    image: alpine:latest
    networks: 
      - my-shared-network
    ...
  another-service-in-its-own-network:
    image: alpine:latest
    networks: 
      - my-private-network
    ...
networks:
  my-shared-network: {}
  my-private-network: {}

В этом последнем примере мы видим, что другая служба в той же сети сможет пропинговать и получить доступ к порту 80 network-example-service , в то время как другая служба в своей собственной сети выиграет.

 

Настройка томов

Существует три типа томов: анонимный , именованный и хост .

Docker управляет как анонимными, так и именованными томами , автоматически монтируя их в самостоятельно созданные каталоги на хосте. В то время как анонимные тома были полезны в более старых версиях Docker (до 1.9), теперь рекомендуется использовать именованные тома. Тома хоста также позволяют нам указать существующую папку на хосте.

Мы можем настроить хост-тома на уровне службы, а именованные тома — на внешнем уровне конфигурации, чтобы сделать последние видимыми для других контейнеров, а не только для того, к которому они принадлежат:

 

services:
  volumes-example-service:
    image: alpine:latest
    volumes: 
      - my-named-global-volume:/my-volumes/named-global-volume
      - /tmp:/my-volumes/host-volume
      - /home:/my-volumes/readonly-host-volume:ro
    ...
  another-volumes-example-service:
    image: alpine:latest
    volumes:
      - my-named-global-volume:/another-path/the-same-named-global-volume
    ...
volumes:
  my-named-global-volume: 

Здесь оба контейнера будут иметь доступ для чтения/записи к общей папке my-named-global-volume , независимо от того, на какой путь они ее сопоставили. Вместо этого два хост-тома будут доступны только для Volumes-Example-Service .

Папка /tmp файловой системы хоста сопоставляется с папкой /my-volumes/host-volume контейнера. Эта часть файловой системы доступна для записи, что означает, что контейнер может читать, а также записывать (и удалять) файлы на хост-компьютере.

Мы можем смонтировать том в режиме только для чтения, добавив к правилу :ro , например, для папки /home (мы не хотим, чтобы контейнер Docker по ошибке удалял наших пользователей).

 

Объявление зависимостей

Часто нам нужно создать цепочку зависимостей между нашими сервисами, чтобы одни сервисы загружались раньше (и выгружались после) других. Мы можем добиться этого результата с помощью ключевого слова depend_on :

services:
  kafka:
    image: wurstmeister/kafka:2.11-0.11.0.3
    depends_on:
      - zookeeper
    ...
  zookeeper:
    image: wurstmeister/zookeeper
    ...

Однако мы должны знать, что Compose не будет ждать завершения загрузки службы zookeeper перед запуском службы kafka ; он просто будет ждать его запуска. Если нам нужно, чтобы служба была полностью загружена перед запуском другой службы, нам нужно получить более глубокий контроль над порядком запуска и завершения работы в Compose .

 

Управление переменными среды

В Compose легко работать с переменными среды. Мы можем определить статические переменные среды, а также динамические переменные с помощью нотации ${} :

services:
  database: 
    image: "postgres:${POSTGRES_VERSION}"
    environment:
      DB: mydb
      USER: "${USER}"

Существуют разные способы предоставления этих значений для Compose.

Например, один из методов устанавливает их в файле .env в том же каталоге, структурированном как файл .properties , ключ=значение :

POSTGRES_VERSION=alpine
USER=foo

В противном случае мы можем установить их в ОС перед вызовом команды:

export POSTGRES_VERSION=alpine
export USER=foo
docker-compose up

Наконец, мы можем легко использовать простую однострочную команду в оболочке:

POSTGRES_VERSION=alpine USER=foo docker-compose up

Мы можем смешивать подходы, но давайте помнить, что Compose использует следующий порядок приоритетов, заменяя менее важные более высокими приоритетами:

  • Создать файл
  • Переменные среды оболочки
  • Файл среды
  • Докерфайл
  • Переменная не определена

 

Масштабирование и реплики

В более старых версиях Compose нам разрешалось масштабировать экземпляры контейнера с помощью команды масштабирования docker-compose . В более новых версиях он устарел и заменен параметром – – scale .

Мы можем использовать Docker Swarm , кластер Docker Engines, и декларативно автомасштабировать наши контейнеры через атрибут replicas в разделе deploy :

services:
  worker:
    image: dockersamples/examplevotingapp_worker
    networks:
      - frontend
      - backend
    deploy:
      mode: replicated
      replicas: 6
      resources:
        limits:
          cpus: '0.50'
          memory: 50M
        reservations:
          cpus: '0.25'
          memory: 20M
      ...

При развертывании мы также можем указать многие другие параметры, например пороговые значения ресурсов. Однако Compose рассматривает весь раздел deploy только при развертывании в Swarm и игнорирует его в противном случае.

 

Реальный пример: Spring Cloud Data Flow

В то время как небольшие эксперименты помогают нам понять отдельные механизмы, наблюдение за реальным кодом в действии определенно раскроет общую картину.

Spring Cloud Data Flow — сложный проект, но достаточно простой, чтобы его можно было понять. Давайте загрузим его файл YAML и запустим:

DATAFLOW_VERSION=2.1.0.RELEASE SKIPPER_VERSION=2.0.2.RELEASE docker-compose up 

Compose скачает, настроит и запустит каждый компонент, а затем объединит журналы контейнера в единый поток в текущем терминале .

Он также применит уникальные цвета к каждому из них для отличного взаимодействия с пользователем:

Мы можем получить следующую ошибку при запуске новой установки Docker Compose:

lookup registry-1.docker.io: no such host

Хотя существуют разные решения этой распространенной ловушки, использование 8.8.8.8 в качестве DNS, вероятно, является самым простым.

 

Управление жизненным циклом

Теперь давайте подробнее рассмотрим синтаксис Docker Compose:

docker-compose [-f <arg>...] [options] [COMMAND] [ARGS...]

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

 

Запуск

Мы видели, что мы можем создавать и запускать контейнеры, сети и тома, определенные в конфигурации, с помощью up :

docker-compose up

Однако после первого раза мы можем просто использовать start для запуска служб:

docker-compose start

Если имя нашего файла отличается от имени по умолчанию ( docker-compose.yml ), мы можем использовать флаги -f и --file для указания альтернативного имени файла:

docker-compose -f custom-compose-file.yml start

Compose также может работать в фоновом режиме как демон при запуске с параметром -d :

docker-compose up -d

 

Дополнительная информация

Ниже приведены несколько часто используемых полезных команд Docker Compose.

  • Команда предназначена для остановки и удаления контейнеров, которые были созданы с помощью docker-compose up
docker-compose down
  • Чтобы безопасно остановить активные службы, мы можем использовать команду stop , которая сохранит контейнеры, тома и сети вместе со всеми внесенными в них изменениями:
docker-compose stop
  • Ознакомиться с журналами сервисов можно выполнив команду:
docker-compose logs -f [service name]
  • Посмотреть список используемых сервисов можно командой (на примере данного проекта):
docker-compose ps
  • Чтобы запустить команду в работающем контейнере используется следующий синтаксис:
docker-compose exec [service name] [command]
  • Увидеть в терминале список образов можно, введя команду:
docker-compose images