Перейти к содержанию

Введение в Docker Compose

При интенсивном использовании Docker управление несколькими контейнерами становится громоздким.

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

В этом руководстве рассмотрим его основные функции и мощные механизмы.

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

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

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

docker-compose up

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

В этом файле нужно указать версию формата файла Compose, хотя бы один сервис и, возможно, volumes и networks:

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

Посмотрим, что это за элементы на самом деле.

Services

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

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

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

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

Volumes и Networks

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

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

Узнаем о них больше в следующем разделе.

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

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

Получение образа

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

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

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
    ...

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

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: {}

В этом последнем примере видим, что another-service-in-the-same-network сможет пропинговать и получить доступ к порту 80 network-example-service, в то время как another-service-in-its-own-network не может.

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

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

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, key=value:

POSTGRES_VERSION=alpine
USER=foo

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

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

Наконец, может пригодиться простой однострочный код в shell:

POSTGRES_VERSION=alpine USER=foo docker-compose up

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

  • файл Compose;
  • переменные среды оболочки;
  • файл среды;
  • Dockerfile;
  • переменная не определена.

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

В более старых версиях Compose разрешалось масштабировать экземпляры контейнера с помощью команды docker-compose scale. Более новые версии устарели и заменили его опцией –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

При deploy можно указать другие параметры, например, пороговые значения ресурсов. Однако 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

Отключение

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

docker-compose stop

Вместо этого, чтобы сбросить статус проекта, просто запустим down, что уничтожит все, кроме внешних томов:

docker-compose down

Заключение

В этом руководстве узнали о Docker Compose и о том, как он работает.

Найти исходный файл docker-compose.yml можно на GitHub, а также ряд полезных тестов, доступных на следующем скриншоте:

Оригинал