Эта публикация - текстовый вариант и сценарий для видео на YouTube (оно удобно разбито на эпизоды).

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

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

У compose есть официальная спецификация, которая детально рассказывает о том, как ее реализация должна работать. Ссылку я оставлю в описании. Ее не то чтобы стоит читать, так как там очень много текста, который вам не поможет так хорошо как эта статья или документация с сайта Docker, но знайте, что спецификация есть и к ней можно обратиться в случае каких-то проблем.

Чтобы воспользоваться Compose, вам необходимо выполнить лишь три шага:

  1. Создайте Dockerfile для вашего приложения, либо же воспользуйтесь docker image с какого-нибудь репозитория, например, DockerHub

  2. Создайте Docker compose файл, в котором опишите сервисы, которые должны работать вместе в изолированной среде.

  3. Запустите docker compose up и Docker запустит все ваше приложение

Compose application model

Спецификация Compose позволяет определить платформонезависимое контейнерное приложение. Такое приложение является набором контейнеров, которые должны работать совместно с адекватным разделением ресурсов и каналом коммуникации.

В Docker используется термин mount, который используется для обозначения привязки какого-то ресурса, например папки с файлами, к контейнеру. Я не нашел красивого русского обозначения, поэтому буду употреблять слово “связан”. То есть это не абстрактная связь, а конкретная связь контейнера и каких-то данных вне контейнера. Учитывайте это по ходу видео.

Компоненты приложения называются сервисами.

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

Сервисы взаимодействуют между собой через сеть (network). В Compose network - это еще одна абстракция, которая позволяет устанавливать IP соединения между контейнерами, внутри которого сервисы могут коммуницировать друг с другом.

Сервисы хранят и разделяют между собой данные в volumes. Мы говорили немного о volumes в прошлой статье о Dockerfile. Если не смотрели его, то посмотрите, там много интересного и подробного. Спецификация compose описывает то какие данные и где должны храниться.

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

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

project - это индивидуальное размещение (деплой) приложения на конкретной платформе. То есть тот docker compose файл, который вы напишите - это отдельный проект. Имя проекта используется для того, чтобы группировать ресурсы вместе и изолировать их от других приложений или других таких же запущенных проектов.

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

Изображение взято из документации Docker.
Изображение взято из документации Docker.

Это пример приложения с фронтендом и бэкендом.

frontend сконфигурирован в рантайме при помощи HTTP Configuration файла, а также HTTPS сертификатом, который встроен в secret store приложения - то место, где хранятся секреты.

backend хранит данные в постоянном хранилище. Оба сервиса взаимодействуют между собой при помощи изолированной backend сети, фронтенд также состоит во фронтенд сети, открывая https порт 443 наружу.

Это приложение состоит из следующих частей:

  • 2 сервиса - frontend с image webapp и backend с image database

  • 1 секрета - HTTPS-сертификата, включенных во frontend

  • 1 конфигурации, включенной во frontend

  • 1 постоянного хранилища - volume, связанного с backend

  • 2 сетей - frontend и backend сети

Так описан compose-файл

services: 
  frontend: 
    image: example/webapp    
    ports:    
      - "443:8043" 
    networks:  
      - front-tier  
      - back-tier    
    configs:    
      - httpd-config 
    secrets:    
      - server-certificate
  backend:  
    image: example/database 
    volumes:    
      - db-data:/etc/data 
    networks:    
      - back-tier

volumes:  
  db-data:  
    driver: flocker   
    driver_opts:     
      size: "10GiB"

configs: 
  httpd-config:  
    external: true

secrets: 
  server-certificate:  
    external: true

networks: 
  front-tier: {} 
  back-tier: {}

В этом примере показаны различия между volume, secret и config:

  • они все связаны с контейнером, однако только volume может изменять данные

  • secrets и configs readonly

Compose file

Compose файл - это .yaml файл, который определяет конфигурацию приложения, а именно версию файла, сервисы - контейнеры, которые должны быть запущены, сети, volumes, конфигурации и secrets.

По умолчанию используется имя compose.yaml или docker-compose.yaml, однако предпочтительно использовать первый вариант compose.yaml

Несколько compose файлов могут быть объединены вместе. Такое объединение перезаписывает или дополняет другой compose файл, собирая несколько файлов в один.

Вы можете использовать фрагменты, расширения или include-команду для работы с несколькими Compose файлами.

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

В нем поддерживаются списки, ассоциативные массивы или мапы. Вы можете ознакомиться с синтаксисом на сайте yaml.org. Однако я верю, что на приведенных в этой статье примерах вы сможете понять как писать и читать файлы с таким форматом данных.

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

На предыдущем примере на верхнем уровне находятся элементы services, networks, secrets, configs, volumes.

Версия Docker compose файла

В начале docker compose файла на верхнем уровне можно указать версию compose - она опциональна. Она используется для обратной совместимости и только предоставляет информацию для разработчиков.

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

Имя Docker compose файла

Имя указывает на то, как должно называться приложение, которое создается при помощи Compose файла. Если вы не укажите его явно, то оно будет сгенерировано. Как только имя определено, к нему можно обратиться по ключу COMPOSE_PROJECT_NAME

services:
  foo:
    image: busybox
    command: echo "I'm running ${COMPOSE_PROJECT_NAME}"

Service

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

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

В Compose файле должен быть верхнеуровневый элемент services, который состоит из пар ключ: значение, где ключ - это название сервиса и значение - его атрибуты.

Вы можете использовать как существующий image, который будет пулиться из какого-нибудь registry, так и собирать image при помощи Dockerfile, указав к нему путь.

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

Атрибут build - описывает путь к Dockerfile из которого будет собран image, а также параметры для этого Dockerfile, например, контекст.

services:
  frontend:
    image: example/webapp
    build: ./webapp

  backend:
    image: example/database
    build:
      context: backend
      dockerfile: ../backend.Dockerfile

В этом примере сервисы frontend и backend собираются из Dockerfile, который находится соответственно в ./webapp и ../backend.Dockerfile

command - переопределяет инструкцию CMD, которая указана в Dockerfile этого сервиса, таким образом именно эта команда будет использоваться при запуске контейнера, а не та, что была указана в Dockerfile. Этот атрибут принимает как shell, так и exec форму команды, как и инструкция CMD.

configs - позволяет сервису использовать конфигурации, про которые мы говорили раньше в этом видео.

services: 
  redis:   
    image: redis:latest  
    configs:         
      - my_config    
      - my_other_config

configs:  
  my_config:  
    file: ./my_config.txt     
  my_other_config:    
    external: true

container_name определяет имя для контейнера, который представлен этим сервисом. Этот атрибут принимает строку - название контейнера.

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

services: 
  web:   
    build: .
    depends_on: 
      - db    
      - redis 
  redis:   
    image: redis 
  db:  
    image: postgres

Здесь сервис web зависит от базы даных и от Redis, контейнер web будет запущен только после того, как будут запущены контейнеры Redis и базы данных. Это нужно для того, чтобы ваше приложение не упало, попытавшись подключиться к еще не поднятому контейнеру, от которого оно зависит. Как и в локальной разработке, если вы пользуетесь базой, то она должна быть запущена до того, как вы запустите приложение.

entrypoint переопределяет ENTRYPOINT, который будет вызван при запуске контейнера. В прошлой статье мы узнали, что в Dockerfile выполняется только последняя инструкция ENTRYPOINT, но здесь мы ее переопределяем и можем задать новое поведение при запуске контейнера.

entrypoint:  
  - php  
  - -d 
  - zend_extension=/usr/local/lib/php/extensions/no-debug-non-zts-20100525/xdebug.so 
  - -d  
  - memory_limit=-1  
  - vendor/bin/phpunit

env_file указывает путь к файлу с переменными, которые будут использоваться для работы контейнера. Чтобы не указывать все переменные, вы можете поместить их в один файл и сослаться на него. Вы можете передать как путь к отдельному файлу, так и список таких путей. По умолчанию это файл .env .

env_file:  
  - ./a.env  
  - ./b.env

environment определяет пары ключ: значение для переменных, которые нужны при работе контейнера. Вы можете использовать два разных синтаксиса - map и list.

environment: 
  RACK_ENV: development 
  SHOW: "true" 
  USER_INPUT:
environment:   
  - RACK_ENV=development  
  - SHOW=true   
  - USER_INPUT

Если вы указали и env_file и env переменные, тогда переменные из env имеют приоритет и будут перезаписывать переменные с таким же названием из env файла.

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

image: redis
image: redis:5
image: redis@sha256:0ed5d5928d4737458944eb604cc8509e245c3e19d02ad83935398bc4b991aac7
image: library/redis
image: docker.io/library/redis
image: my_private.registry:5000/redis

networks - указывает список network, к которым принадлежит сервис.

services: 
  some-service:   
    networks:    
      - some-network  
      - other-network

В этом примере сервис some-service принадлежит к двум сетям, которые были описаны в Compose файле.

ports определяет порты, которые отображаются наружу из контейнера. Мы говорили в прошлой статье, что EXPOSE в Dockerfile не открывает порты, а лишь говорит разработчикам о том, что этот порт нужен для работы приложения. А вот в Compose файле ports как раз и открывает порты наружу.

ports: 
  - "3000" 
  - "3000-3005" 
  - "8000:8000"
  - "9090-9091:8080-8081" 
  - "49100:22" 
  - "8000-9000:80" 
  - "6060:6060/udp"

Таким образом обратившись к localhost:3000 вы попадете внутрь контейнера на порт 3000. Вы можете поменять порт и обращаться к другому локальному порту, который в свою очередь будет отображен в контейнер. Порты на локальной машине должны быть доступны, иначе Compose файл не будет запущен.

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

Возможные значения:

  • always - image всегда пулятся

  • never - не пулятся никогда и должны быть на локальной машине, чтобы Compose файл мог ими воспользоваться

  • missing - Docker пулит только недостающие image

  • build - Docker будет создавать image из Dockerfile каждый раз

restart - определяет правила, по которым контейнер будет перезапускаться, когда он упал.

Есть несколько опций:

  • no - по умолчанию. Docker не перезапускает контейнер если он упал

  • always - Docker всегда перезапускает контейнер, пока он не будет удален

  • on-failure - Docker перезапускает контейнер, если он упал из-за ошибки, а не был остановлен правильно

  • unless-stopped - Docker перезапускает контейнер, пока сервис не будет остановлен или удален

restart: "no"
restart: always
restart: on-failure
restart: unless-stopped

secrets - позволяет сервису обратиться к секретам, которые были указаны в текущем Compose файле.

services: 
  frontend:   
    image: example/webapp 
    secrets:      
      - server-certificate

secrets: 
  server-certificate:  
    file: ./server.cert

volumes определяет путь хранения volume из контейнера на локальном хосте. Вы можете создать volume в какой-нибудь директории локально или же сослаться на описанный volume из текущего Compose файла.

services: 
  backend:  
    image: example/backend   
    volumes:    
      - type: volume 
        source: db-data   
        target: /data     
        volume:     
          nocopy: true
      - type: bind       
        source: /var/run/postgres/postgres.sock  
        target: /var/run.postgres/postgres.sock 

volumes: 
  db-data:

Network

Networks или сети позволяют сервисам общаться между собой. Поместив несколько сервисов в один network, они могут свободно обращаться друг к другу по названию сервиса. Это удобно и быстро конфигурируется, что делает деплой приложения в Docker простым.

В сервисе мы рассмотрели атрибут networks, в котором указывается список сетей, к которым принадлежит сервис.

Сейчас на примерах мы рассмотрим то, как настроить сеть и какие атрибуты у нее есть.

services: 
  frontend: 
    image: example/webapp 
    networks:     
      - front-tier  
      - back-tier

networks:
  front-tier: 
  back-tier:

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

services:
  proxy: 
    build: ./proxy  
    networks:   
      - frontend
  app:   
    build: ./app  
    networks:    
      - frontend  
      - backend  
  db:  
    image: postgres   
    networks:    
      - backend

networks:
  frontend:   
    driver: custom-driver-1 
  backend:   
    driver: custom-driver-2 
    driver_opts:   
      foo: "1" 
      bar: "2"

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

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

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

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

Атрибут driver указывает на то, какой драйвер для работы сети следует использовать. Будет выброшена ошибка, если драйвер недоступен для использования при запуске Compose файла.

Есть два типа драйверов, которые поддерживаются Compose: host и nonehost использует возможности хоста, то есть того компьютера, на котором запущен Compose файл, для коммуникации между сервисами, none отключает коммуникацию.

networks: 
  db-data:   
  driver: overlay

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

networks: 
  db-data:  
    driver_opts: 
      foo: "bar"     
      baz: 1

Если атрибут attachable установлен в true, тогда контейнеры вне этого Compose файла могут подключиться к сети и общаться с сервисами внутри этой сети.

networks: 
  mynet1:   
    driver: overlay   
    attachable: true

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

services:
  proxy:
    image: example/proxy
    networks:
      - outside
      - default
  app:
    image: example/app
    networks:
      - default

networks:
  outside:
    external: true

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

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

Атрибут name устанавливает имя для сети, как и с volume, вы можете использовать переменные для подстановки значений при запуске Compose файла.

networks:  
  network1:   
    external: true  
    name: "${NETWORK_ID}"

Если вы используете внешний network, тогда вы можете указать настоящее имя сети, а внутри текущего Compose файла обращаться к сети с другим именем.

Volume

Volumes - это постоянные хранилища данных вашего контейнера.

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

Верхнеуровневая конфигурация volumes позволяет описать volumes, которые будут использованы сервисами. Как мы заметили раньше, в сервисе нужно явно указывать то, какие данные будут сохраняться и куда.

Давайте рассмотрим несколько примеров конфигурации volumes.

services:
  backend:
    image: example/database
    volumes:
      - db-data:/etc/data
  backup:
    image: backup-service
    volumes:
      - db-data:/var/lib/backup/data

volumes:
  db-data:

На этом примере мы видим два сервиса, один из которых база данных, а второй - бэкап сервис, делающий бэкапы базы данных. Оба этих сервиса имеют доступ к данным из базы данных благодаря volume.

Сервис базы данных сохраняет данные в volume db-data, а сервис бэкапа сохраняет эти данные в своем контейнере по пути /etc/data. Таким образом данные из одного контейнера попадают во второй.

У конфигурации volume есть несколько атрибутов, но они не являются обязательными.

driver указывает какой драйвер для volume должен быть использован. Некоторые могут быть платформозависимыми. Если драйвер недоступен, то Docker выбросит ошибку при попытке запуске Compose файла.

volumes:  
  db-data:   
    driver: foobar

driver_opts определяет опции volume, пар ключ: значение, они тоже платформозависимые, то есть на Windows и на Linux эти опции могут отличаться.

volumes:
  example:
    driver_opts:
      type: "nfs"
      o: "addr=10.40.0.199,nolock,soft,rw"
      device: ":/docker/example"

Если атрибут external установлен в true, тогда Docker ожидает, что этот volume уже существует и управляется из другого Compose файла. Если такого volume не будет, то выбросится ошибка при запуске Compose файла. Также в этом случае все атрибуты кроме имени не будут учитываться, раз volume был создан и управляется из другого места. Но если эти атрибуты отличаются от оригинальных, то будет выброшена ошибка. Таким образом определяя external как true, вы можете указать только имя volume для этого Compose файла.

services:
  backend:
    image: example/database
    volumes:
      - db-data:/etc/data

volumes:
  db-data:
    external: true

На этом примере volume не создается, а ищется уже существующий.

name устанавливает имя для volume. Вы можете воспользоваться подстановкой переменных при запуске Compose файла. В данном случае имя volume будет получено из переменной DATABASE_VOLUME из .env файла.

volumes:
  db-data:
    name: ${DATABASE_VOLUME}
volumes:
  db-data:
    external:
      name: actual-name-of-volume

В этом примере внешний volume имеет имя actual-name-of-volume, а внутри текущего Compose файла вы можете обратиться по имени db-data .

Config

Config позволяет сервисам изменять их поведение без необходимости изменения используемых image.

Сервисы могут обратиться к конфигам только если имеют атрибут configs.

Как и volume, конфиги связаны с файловой системой контейнера. По умолчанию они находятся в корне файловой системы контейнера.

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

  • file - создается конфиг со всем содержимым файла

  • environment - создается конфиг со значением переменной

  • content - создается конфиг со значением, которое было передано в этом атрибуте

external - Если указано как true, Docker ожидает, что конфиг был создан, он не пытается его создать заново и выбрасывает ошибку, если такого конфига не существует.

name - имя конфига, которое Docker ищет.

configs: 
  http_config:  
    file: ./httpd.conf

В этом примере создается config, значением которого будет содержимое файла. Имя для него будет <project_name>_http_config.

configs:
  app_config:
    content: |
      debug=${DEBUG}
      spring.application.admin.enabled=${DEBUG}
      spring.application.name=${COMPOSE_PROJECT_NAME}

В этом случае создается конфиг, содержимым которого является указанный текст.

Secret

Секреты используются для использования чувствительных данных. Сервисы могут обратиться к секретам только если имеют атрибут secrets.

На верхнем уровне элемент secrets определяет или ссылается на те самые чувствительные данные, источником которых служит либо file либо environment.

  • Если источник file, тогда создается секрет с содержимым этого файла.

  • Если источник environment, тогда создается секрет со значением этой переменной.

external обозначает то, что секрет уже был создан и Docker не пытается его создать. Если он не был создан, тогда вы получите ошибку при выполнении Compose файла.

name - название секрета в Docker

Давайте рассмотрим несколько примеров.

secrets: 
  server-certificate:  
    file: ./server.cert

В этом примере создается секрет с названием <project_name>_server-certificate в момент запуска Compose файла, а значение секрета берется из файла server.cert.

secrets:  
  token:  
    environment: "OAUTH_TOKEN"

В этом случае секрет называется как <project_name>_token и создается во время запуска Compose файла, значением является значение переменной.

secrets:  
  server-certificate:  
    external: true

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

Fragments

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

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

Фрагменты создаются при помощи символы амперсанд & и имени фрагмента. Они устанавливаются на ключи YAML файла, раз этот формат - это ключ-значение, то подстановки происходят после двоеточия. С помощью * и имени можно обратиться к этому фрагменту и получить его значение.

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

volumes:  
  db-data: &default-volume
    driver: default 
  metrics: *default-volume

В данном примере создается фрагмент default-volume, который подставит все свойства volume db-data в volume metrics, то есть metrics также будет иметь свойство driver: default.

services:
  first:
    image: my-image:latest
    environment: &env
      - CONFIG_KEY
      - EXAMPLE_KEY
      - DEMO_VAR
  second:
    image: another-image:latest
    environment: *env

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

Иногда вы можете хотеть переопределить значения фрагмента только частично, при помощи YAML merge вы можете это сделать как в данном примере.

services:
  backend:
    image: example/database
    volumes:
      - db-data
      - metrics
volumes:
  db-data: &default-volume
    driver: default
    name: "data"
  metrics:
    <<: *default-volume
    name: "metrics"

В данном примере default-volume подставит только driver, а name подставлен не будет, так как мы его явно указали.

Вы также можете дополнить значения фрагмента дополнительными элементами как в этом примере.

services:
  first:
    image: my-image:latest
    environment: &env
      FOO: BAR
      ZOT: QUIX
  second:
    image: another-image:latest
    environment:
      <<: *env
      YET_ANOTHER: VARIABLE

Но в таком случае вы должны указывать переменные в формате Ключ: Значение, а не - Ключ=Значение.

Если вы хотите использовать фрагменты в нескольких файлах, тогда воспользуйтесь расширениями Extension.

Extensions

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

Для этого на верхнем уровне нужно определить элемент, который начинается с x-. Так Docker поймет, что это расширение и будет знать о нем.

Давайте рассмотрим несколько примеров:

x-env: &env
  environment:
    - CONFIG_KEY
    - EXAMPLE_KEY
 
services:
  first:
    <<: *env
    image: my-image:latest
  second:
    <<: *env
    image: another-image:latest

В первом примере определен extension x-custom и x-foo, мы можем добавить x-foo в сервис и Docker его обработает, но ничего не сделает, однако значение x-foo можно получить в самом приложении webapp.

Во втором примере env не принадлежит ни одному из сервисов. Такой extension определяет новую ноду, в которой содержится поле environment. А якорь используется для того, чтобы обратиться к этому полю в двух сервисах.

x-environment: &default-environment
  FOO: BAR
  ZOT: QUIX
x-keys: &keys
  KEY: VALUE
services:
  frontend:
    image: example/webapp
    environment: 
      << : [*default-environment, *keys]
      YET_ANOTHER: VARIABLE

В этом примере можно использовать объединение из нескольких расширений, тогда все ключи из обоих расширений будут подставлены. Как и в случае с фрагментами, вы должны указывать переменные в формате Ключ: Значение, а не - Ключ=Значение.

Interpolation

Как и в Dockerfile, мы можем воспользоваться wildcard для подстановки значений переменных из конфигурационного файла.

Значение переменной можно получить написав ${VARIABLE} или $VARIABLE. Можно обратиться к вложенным переменным, например как здесь.

${VARIABLE:-${FOO}}
${VARIABLE?$FOO}
${VARIABLE:-${FOO:-default}}

Есть несколько видов подстановки.

${VARIABLE:-default} - подставит default, если переменная VARIABLE не задана или пустая

${VARIABLE-default} - подставит default только если переменная VARIABLE не задана

${VARIABLE:?err} - завершит программу с сообщением об ошибке err если переменная VARIABLE не задана или пустая

${VARIABLE?err} - завершит программу с сообщением об ошибке err только если переменная VARIABLE не задана

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

Если используется слияние нескольких Compose файлов, тогда подстановка происходит до слияния, чтобы не допустить ошибок.

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

services: 
  foo:  
    labels:    
      "$VAR_NOT_INTERPOLATED_BY_COMPOSE": "BAR"

Merge

Вы можете использовать несколько compose файлов и объединить их в один, чтобы собрать свое приложение.

Давайте рассмотрим правила, которые соблюдает merge.

Если мержутся мапы, то есть пары ключ-значение, то недостающие ключи добавляются, а существующие перезаписываются.

#первый файл
services: 
  foo: 
    key1: value1
    key2: value2

#второй файл
services: 
  foo: 
    key2: VALUE 
    key3: value3

#результат
services: 
  foo: 
    key1: value1 
    key2: VALUE 
    key3: value3

Если мержутся последовательности, тогда они дополняются.

#первый файл
services: 
  foo:   
    DNS:   
      - 1.1.1.1

#второй файл
services: 
  foo:   
    DNS:   
      - 8.8.8.8

#результат
services:  
  foo: 
    DNS:  
      - 1.1.1.1 
      - 8.8.8.8

COMMAND, ENTRYPOINT, HEALTHCHECK перезаписываются, так как мы помним, что они не могут дублироваться в Dockerfile, то и здесь они не могут использоваться совместно, а только последняя инструкция будет применена.

#первый файл
services:
  foo:   
    command: ["echo", "foo"]

#второй файл
services: 
  foo: 
    command: ["echo", "bar"]

#результат
services: 
  foo:  
    command: ["echo", "bar"]

В случае с уникальными ресурсами, такими как порты, volume, секреты и конфигурации если уникальные значения конфликтуют, тогда они будут перезаписаны.

#первый файл
services:  
  foo:   
    volumes:      
      - foo:/work

#второй файл
services: 
  foo:   
    volumes:     
      - bar:/work

#результат
services:  
  foo:   
    volumes:  
      - bar:/work

В качестве уникальных ресурсов выступают:

Эта публикация - текстовый вариант и сценарий для видео на YouTube (оно удобно разбито на эпизоды).-48
Изображение взято из документации Docker.

Include

Вы можете сослаться на другой Compose файл если вы хотите:

  1. Переиспользовать Compose файлы,

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

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

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

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

include:  
  - my-compose-include.yaml #здесь описан serviceB

services: 
  serviceA:   
    build: .  
    depends_on:     
      - serviceB #используем этот сервис как будто он описан прямо в этом файле

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

include: 
  - ${INCLUDE_PATH:?FOO}/compose.yaml

Можно использовать длинный синтаксис и более детально сконфигурировать include.

include:  
  - path: ../commons/compose.yaml    
    project_directory: ..     
    env_file: ../another/.env

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

project_directory определяет путь, относительно которого относительные пути из path будут применены. По умолчанию это папка с текущим Compose файлом.

env_file указывает путь к файлу с переменными, которые будут использоваться в указанных Compose файлах. По умолчанию это .env файл в директории project_directory. Тут так же можно указать как строку так и список строк. Переменные в локальной директории, то есть в той, в которой находится текущий Compose файл имеют преимущество перед импортируемыми и могут их переписать.

Комментарии (3)


  1. yarkov
    31.03.2024 22:01
    +2

    За код картинками отдельное вам спасибо


    1. ilyalisov Автор
      31.03.2024 22:01

      В ближайшее время перепишу на текст!)


      1. vektory79
        31.03.2024 22:01
        +1

        Не просто текст, а оформить как код. Чтобы глаза при чтении не ломать.
        Но за статьи спасибо. Как раз думал как подступиться, чтобы сыновьям докер объяснить :)