Перейти к содержимому

Докер

Docker Fundamentals: что внутри compose.yaml и как там всё устроено

Третья часть серии статей по Docker. Подробный справочник по структуре compose-файла — top-level разделы, services, build, deploy, networks, volumes, configs, secrets, интерполяция переменных, merge-правила и YAML-фрагменты.


Введение

В прошлой части я разобрал хранилища — тома, bind mount, tmpfs. В третьей части (в этой) планировалось добраться до сети. До неё дойдём, но прежде чем разбирать, как сервисы общаются друг с другом, стоит на секунду остановиться и разобрать сам файл, в котором мы всё это описываем. Потому что compose.yaml — это спецификация со своими top-level разделами, правилами слияния, подстановкой переменных и кучей мелких нюансов, которые легко упустить, если читать документацию по верхам.

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

Из чего состоит compose-файл

Compose-файл описывает модель приложения через набор top-level (верхнеуровневых) разделов. Обязателен в основном только один — services, остальные подключаются по необходимости.

Раздел Что описывает Обязателен
services Сервисы приложения — абстракция над контейнерами Да
networks Именованные сети для сервисов Нет, иначе используется неявная сеть default
volumes Именованные тома Нет
configs Неконфиденциальные конфигурационные данные Нет
secrets Чувствительные данные Нет
models AI-модели, которые пуллятся и обслуживаются runner’ом Нет
include Подключение и слияние других compose-файлов Нет
x-* (extensions) Произвольные данные для переиспользования, Compose их игнорирует Нет
version Только для обратной совместимости, ни на что не влияет Нет, и больше не нужен
name Имя проекта по умолчанию Нет, иначе подставляется автоматически

profiles влияют только на services. Все остальные top-level разделы всегда активны, независимо от того, какие профили включены.

version и name

Кому и зачем: если вы только начинаете — можно пропустить. version больше ни на что не влияет, а name нужна только если вы хотите явно задать имя проекта вместо автогенерируемого.

version — поле, которое раньше реально влияло на то, по какой схеме Compose читает файл. Сейчас оно ни на что не влияет: Compose всегда разбирает файл по самой свежей схеме, что бы в version ни было написано. Если поле в файле есть, Compose просто выведет предупреждение, что оно устарело, и продолжит работать как обычно. Смысла писать его в новых файлах нет. (С другой стороны, возможно, оно используется в старых версиях Docker, где применяется старая схема.)

name — имя проекта, которое используется по умолчанию, если вы не задаёте его другим способом (флагом, переменной окружения и т.п.). Имя проекта доступно для интерполяции как COMPOSE_PROJECT_NAME:

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

services

Кому и зачем: всем. Это единственный обязательный раздел compose-файла — без него Compose просто не поймёт, что запускать.

Сервис, по сути, рецепт для одного или нескольких одинаковых контейнеров: какой образ запускать, с какими портами, переменными окружения, томами и так далее. Когда я пишу services.web, я описываю не один конкретный контейнер, а правило, по которому Compose его создаёт. Контейнеров по этому правилу может получиться и несколько одинаковых копий (реплик), если задать scale или deploy.replicas. Удобство в том, что сервис можно масштабировать или пересоздавать отдельно от остальных: например, поднять три копии web, пока db остаётся в одном экземпляре.

У сервиса есть две опциональные секции, и у каждой свой мини-стандарт внутри общего. build описывает, как собрать образ (это Compose Build Specification), deploy, как развернуть сервис и с какими ограничениями (Compose Deploy Specification). Если платформа их не поддерживает, файл всё равно остаётся валидным, просто эти секции игнорируются.

Атрибутов у сервиса очень много, так что разобью их по смыслу.

Образ, сборка и запуск процесса

Атрибут Что делает Пример / примечание
image Образ для запуска контейнера, формат [registry/][project/]image[:tag @digest] image: redis:5
build Конфигурация сборки образа из исходников (см. раздел «build»)
platform Целевая платформа os[/arch[/variant]] platform: linux/arm64/v8
pull_policy Когда и как Compose пуллит образ: always, never, missing (default, alias if_not_present), build, daily, weekly, every_<duration> pull_policy: every_12h
scale Сколько контейнеров поднимать по умолчанию (должно совпадать с deploy.replicas, если задано и то, и то) scale: 3
runtime Какой OCI runtime использовать, по умолчанию runc runtime: runc
provider Передаёт управление жизненным циклом сервиса внешнему бинарю (Compose сам не управляет) см. ниже
read_only Контейнер создаётся с файловой системой только на чтение read_only: true
init Запускает init-процесс (PID 1), который форвардит сигналы и подчищает зомби-процессы init: true
command Переопределяет CMD из образа. null — команда из образа, []/'' — пустая команда command: bundle exec thin -p 3000
entrypoint Переопределяет ENTRYPOINT из образа; если задан и не null, CMD образа игнорируется список или строка, как в Dockerfile
working_dir Переопределяет рабочую директорию (аналог WORKDIR)
user Пользователь, от которого выполняется процесс (аналог USER), иначе root
hostname / domainname Кастомное имя хоста / домена контейнера (валидный RFC 1123)
container_name Своё имя контейнера вместо автогенерируемого. Формат [a-zA-Z0-9][a-zA-Z0-9_.-]+. С ним нельзя масштабировать сервис container_name: my-web-container

Про command: в отличие от CMD в Dockerfile, это поле не выполняется автоматически через SHELL. Если нужна интерполяция переменных шеллом — оборачивайте сами: command: /bin/sh -c 'echo "hello $$HOSTNAME"'.

Про provider отдельно, потому что без объяснения этот пример выглядит как магия. Короче: иногда нужный «сервис» — это не контейнер вообще, а что-то внешнее, например, облачная база данных, которую поднимает не Docker, а сторонняя программа. provider говорит Compose: «Не пытайся создавать контейнер для этого сервиса сам, вызови вот эту программу, и пусть она разбирается».

services:
  database:
    provider:
      type: awesomecloud
      options:
        type: mysql
        foo: bar
  app:
    image: myapp
    depends_on:
      - database
services:
  database:
    provider:
      type: awesomecloud
      options:
        type: mysql
        foo: bar
  app:
    image: myapp
    depends_on:
      - database

Что здесь происходит по шагам:

  1. У сервиса database нет image, потому что контейнер для него вообще не будет создан.
  2. При docker compose up Compose видит provider.type: awesomecloud и запускает внешнюю программу с этим именем, передав ей всё, что лежит в options (type: mysql, foo: bar). Дальше создание и настройка самой базы — целиком забота этой программы, не Compose.
  3. Когда awesomecloud подготовит базу, она возвращает Compose какие-то данные о ней, допустим, адрес для подключения и ключ доступа (URL и API_KEY).
  4. Compose передаёт эти данные сервису app, потому что он зависит от database (depends_on). Передаёт через переменные окружения, и к именам переменных приклеивает имя сервиса-провайдера в верхнем регистре — получаются DATABASE_URL и DATABASE_API_KEY.
  5. Внутри контейнера app можно просто прочитать DATABASE_URL и подключиться, неважно, что реальная база крутится не в Docker, а где-то у облачного провайдера. При docker compose down то же самое в обратную сторону: контейнер удалять не нужно (его и не было), вместо этого Compose попросит awesomecloud снести то, что он создал.

Жизненный цикл и зависимости

Атрибут Что делает
restart Политика перезапуска: no (по умолчанию), always, on-failure[:max-retries], unless-stopped
healthcheck Проверка “здоровья” контейнера, переопределяет HEALTHCHECK из образа
depends_on Порядок запуска/остановки сервисов
profiles Список профилей, при которых сервис активен (см. раздел «profiles»)
post_start Хуки, выполняемые после старта контейнера
pre_stop Хуки перед остановкой контейнера (не сработают при аварийном завершении)
stop_signal Сигнал для остановки, по умолчанию SIGTERM
stop_grace_period Сколько ждать перед SIGKILL, по умолчанию 10 секунд

healthcheck пример:

healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost"]
  interval: 1m30s
  timeout: 10s
  retries: 3
  start_period: 40s
  start_interval: 5s
healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost"]
  interval: 1m30s
  timeout: 10s
  retries: 3
  start_period: 40s
  start_interval: 5s

test может быть строкой (тогда это эквивалент CMD-SHELL, команда выполняется через /bin/sh на Linux) или списком, где первый элемент — NONE, CMD или CMD-SHELL. Чтобы выключить healthcheck из образа — test: NONE или disable: true.

depends_on — короткий и длинный синтаксис:

# короткий — просто порядок запуска, без ожидания healthy
services:
  web:
    depends_on:
      - db
      - redis
 
# длинный — с условиями
services:
  web:
    depends_on:
      db:
        condition: service_healthy
        restart: true
      redis:
        condition: service_started
# Запускает db и redis, ждёт healthcheck для db и запуска redis, затем запускает web.
# короткий — просто порядок запуска, без ожидания healthy
services:
  web:
    depends_on:
      - db
      - redis
 
# длинный — с условиями
services:
  web:
    depends_on:
      db:
        condition: service_healthy
        restart: true
      redis:
        condition: service_started
# Запускает db и redis, ждёт healthcheck для db и запуска redis, затем запускает web.
Параметр длинного синтаксиса Значение
condition: service_started То же самое, что короткий синтаксис
condition: service_healthy Ждать, пока зависимость не станет healthy (здоров и готов работать)
condition: service_completed_successfully Ждать успешного завершения зависимости
restart: true Перезапускать этот сервис после обновления зависимости. Касается только явного рестарта через Compose, не автоматического рестарта runtime’а
required: false Не падать, если зависимость недоступна — только предупреждение

post_start / pre_stop устроены одинаково:

services:
  test:
    post_start:
      - command: ./do_something_on_startup.sh
        user: root
        privileged: true
        environment:
          - FOO=BAR
services:
  test:
    post_start:
      - command: ./do_something_on_startup.sh
        user: root
        privileged: true
        environment:
          - FOO=BAR

Здесь после старта контейнера test Compose выполнит внутри него команду ./do_something_on_startup.sh — от имени root, с повышенными правами и с дополнительной переменной FOO=BAR в окружении именно этой команды. Сам контейнер при этом продолжает работать как обычно, никто его не перезапускает.

Сеть на уровне сервиса

Атрибут Что делает
ports Публикация портов хост:контейнер. Нельзя использовать с network_mode: host — будет runtime error
expose Открыть порт только для других контейнеров в сети, без публикации на хост
networks К каким именованным сетям подключён сервис, плюс настройки на уровне подключения
network_mode bridge, none, host, service:{name}, container:{name} несовместимо с networks
links Связь с контейнерами другого сервиса по имени/алиасу (не обязательны для общения внутри одной сети)
external_links Связь с сервисами вне текущего Compose-приложения
dns, dns_opt, dns_search Кастомные DNS-серверы, опции резолвера, домены поиска
extra_hosts Дополнительные записи в /etc/hosts
mac_address MAC-адрес контейнера (на уровне сервиса). Некоторые runtime’ы могут отклонить это значение (тогда используйте networks.<name>.mac_address)

ports, короткий синтаксис [HOST:]CONTAINER[/PROTOCOL]:

ports:
  - "3000"
  - "3000-3005"
  - "8000:8000"
  - "9090-9091:8080-8081"
  - "127.0.0.1:8001:8001"
  - "6060:6060/udp"
  - "127.0.0.1:5000-5010:5000-5010"
  - "::1:6000:6000"
  - "[::1]:6001:6001"
ports:
  - "3000"
  - "3000-3005"
  - "8000:8000"
  - "9090-9091:8080-8081"
  - "127.0.0.1:8001:8001"
  - "6060:6060/udp"
  - "127.0.0.1:5000-5010:5000-5010"
  - "::1:6000:6000"
  - "[::1]:6001:6001"

Поясню пару строк, чтоб было понятно. "3000" — задан только порт контейнера, какой порт хоста подставится, решит сам Docker (возьмёт случайный свободный). "8000:8000" — порт хоста 8000 ведёт на порт контейнера 8000, оба фиксированы. "127.0.0.1:8001:8001" — то же самое, но слушать будем только на localhost хоста, а не на всех интерфейсах сразу. "[::1]:6001:6001" — то же самое, но для IPv6-адреса.

Если не указать host IP явно (как в первых трёх строках), Docker слушает на 0.0.0.0, то есть на всех интерфейсах — это может обойти файрвол хоста и открыть порт наружу, если у хоста публичный IP.

Длинный синтаксис портов:

ports:
  - name: web
    target: 80
    host_ip: 127.0.0.1
    published: "8080"
    protocol: tcp
    app_protocol: http
    mode: host
ports:
  - name: web
    target: 80
    host_ip: 127.0.0.1
    published: "8080"
    protocol: tcp
    app_protocol: http
    mode: host

expose:

expose:
  - "3000"
  - "8080-8085/tcp"
expose:
  - "3000"
  - "8080-8085/tcp"

Если в Dockerfile образа уже объявлены порты через EXPOSE, они видны другим контейнерам в сети даже если expose в Compose-файле не задан.

extra_hosts поддерживает короткий синтаксис (список строк) и длинный (маппинг):

# короткий
extra_hosts:
  - "somehost=162.242.195.82"
  - "otherhost=50.31.209.229"
  - "myhostv6=[::1]"

# длинный
extra_hosts:
  somehost: "162.242.195.82"
  otherhost: "50.31.209.229"
# короткий
extra_hosts:
  - "somehost=162.242.195.82"
  - "otherhost=50.31.209.229"
  - "myhostv6=[::1]"

# длинный
extra_hosts:
  somehost: "162.242.195.82"
  otherhost: "50.31.209.229"

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

Под-атрибут networks.<name> Что делает
aliases Альтернативные имена сервиса в этой сети (свои для каждой сети). Алиас может быть shared между несколькими контейнерами и сервисами — тогда к кому именно он резолвится, не гарантируется
ipv4_address, ipv6_address Статический IP (нужен ipam с подходящим subnet в top-level networks)
interface_name Имя сетевого интерфейса внутри контейнера
link_local_ips Список link-local IP
mac_address MAC именно для этой сети
driver_opts Опции драйвера, специфичные для подключения
gw_priority Сеть с наибольшим значением становится дефолтным шлюзом. По умолчанию 0
priority Порядок подключения сервиса к сетям. Не влияет на выбор шлюза и не контролирует имя интерфейса (eth0 и т.п.) — для этого нужен interface_name
services:
  backend:
    networks:
      back-tier:
        aliases:
          - database
      admin:
        aliases:
          - mysql
services:
  backend:
    networks:
      back-tier:
        aliases:
          - database
      admin:
        aliases:
          - mysql

Сервис backend подключён сразу к двум сетям, и в каждой у него своё “прозвище”. Контейнеры в сети back-tier могут достучаться до него по имени database, а контейнеры в сети admin по имени mysql. Имя самого сервиса (backend) при этом тоже продолжает работать как обычно. Если networks не задан вообще, сервис неявно подключается к сети default, это эквивалентно networks: {default: {}}. Чтобы вообще отключить сетевой доступ, network_mode: none.

Данные и конфигурация

Атрибут Что делает
volumes Монтирование томов/bind mount/tmpfs в контейнер (на уровне сервиса)
volumes_from Подключить все тома другого сервиса/контейнера целиком. Можно указать ro/rw и ссылаться на контейнер вне Compose через container:<name>
configs Доступ к конфигам из top-level configs
secrets Доступ к секретам из top-level secrets
env_file Файл(ы) с переменными окружения
environment Переменные окружения напрямую в файле
label_file Файл(ы) с лейблами, альтернатива labels для случаев когда лейблов много
labels Метаданные на контейнере
tmpfs tmpfs-монтирование (короткая форма, без отдельного top-level раздела)

volumes, короткий синтаксис VOLUME:CONTAINER_PATH[:ACCESS_MODE], длинный — объект с type/source/target/read_only/bind/volume/tmpfs/image:

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

configs и secrets устроены практически одинаково: короткий синтаксис просто даёт доступ и монтирует под именем источника, длинный позволяет задать target/uid/gid/mode:

services:
  redis:
    image: redis:latest
    configs:
      - source: my_config
        target: /redis_config
        uid: "103"
        gid: "103"
        mode: 0440
    secrets:
      - source: my-token
        uid: "103"
        gid: "103"
        mode: 0o440
configs:
  my_config:
    external: true
secrets:
  my-token:
    environment: "MY_TOKEN"
services:
  redis:
    image: redis:latest
    configs:
      - source: my_config
        target: /redis_config
        uid: "103"
        gid: "103"
        mode: 0440
    secrets:
      - source: my-token
        uid: "103"
        gid: "103"
        mode: 0o440
configs:
  my_config:
    external: true
secrets:
  my-token:
    environment: "MY_TOKEN"

Нюанс: uid/gid/mode для секретов работают только если источник секрета environment. Если источник file, Compose использует bind-mount, и эти атрибуты тихо игнорируются.

label_file — удобно, когда лейблов много и не хочется засорять Compose-файл:

services:
  one:
    label_file: ./app.labels

  two:
    label_file:
      - ./app.labels
      - ./additional.labels
services:
  one:
    label_file: ./app.labels

  two:
    label_file:
      - ./app.labels
      - ./additional.labels

Формат файла такой же, как у env_file — пары KEY=VALUE. Если несколько файлов, обрабатываются сверху вниз; при конфликте побеждает последний файл. Если одно и то же поле задано и в label_file, и в labels — побеждает labels.

env_file:

env_file:
  - path: ./default.env
    required: true   # по умолчанию
  - path: ./override.env
    required: false
  - path: ./raw.env
    format: raw       # без интерполяции, значения как есть
env_file:
  - path: ./default.env
    required: true   # по умолчанию
  - path: ./override.env
    required: false
  - path: ./raw.env
    format: raw       # без интерполяции, значения как есть

Если переменная задана и в env_file, и в environment — побеждает environment, даже если значение пустое.

Несколько правил парсинга формата .env файла, которые полезно знать:

  • Строки начиная с # — комментарии, игнорируются
  • Разделитель между ключом и значением — = или :
  • Значения в двойных кавычках поддерживают escape-последовательности: \n, \t, \\
  • Значения в одинарных кавычках берутся буквально: VAR='$OTHER'$OTHER
  • Инлайновый комментарий для незакавыченных значений нужно предварять пробелом: VAR=VAL # commentVAL

environment, мапа или список (булевы значения обязательно в кавычках, иначе YAML превратит их в True/False):

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

tmpfs:

services:
  app:
    tmpfs:
      - /data:mode=755,uid=1009,gid=1009
      - /run
services:
  app:
    tmpfs:
      - /data:mode=755,uid=1009,gid=1009
      - /run

Про лейблы и зарезервированный префикс

Compose автоматически проставляет на каждый контейнер два canonical label’а:

  • com.docker.compose.project — имя проекта
  • com.docker.compose.service — имя сервиса из Compose-файла

Префикс com.docker.compose зарезервирован. Если указать лейбл с таким префиксом в Compose-файле, будет runtime error.

Ресурсы и изоляция

CPU

Атрибут Что делает
cpus Сколько (потенциально виртуальных) ядер CPU выделить контейнеру. Число дробное, 0.000 значит “без лимита”. Если задано и здесь, и в deploy.resources.limits.cpus, значения должны совпадать
cpu_count Целое число — сколько именно CPU контейнер может использовать
cpu_percent Какой процент от всех доступных CPU доступен контейнеру
cpu_shares Относительный вес контейнера при распределении CPU между несколькими контейнерами. Это не абсолютное число ядер, а пропорция по сравнению с другими
cpu_period Период CFS-планировщика ядра Linux (Completely Fair Scheduler). Работает в связке с cpu_quota
cpu_quota Сколько времени CPU достаётся контейнеру за один такой период
cpu_rt_runtime Время, которое контейнер может работать в режиме real-time планировщика. Указывается числом микросекунд или duration, например 400ms
cpu_rt_period Период того же real-time планировщика, тоже в микросекундах или duration
cpuset Список или диапазон конкретных ядер, на которых разрешено выполняться: 0-3 или 0,1

Память

Атрибут Что делает
mem_limit Жёсткий лимит памяти в байтовом формате (512m, 1g…). Должен совпадать с deploy.resources.limits.memory, если задан и там, и там
mem_reservation Гарантированный резерв памяти, тот же формат. Сверяется с deploy.resources.reservations.memory
mem_swappiness Число от 0 до 100. Показывает, насколько активно ядро хоста выгружает память контейнера в swap: 0 — не выгружать вообще, 100 — выгружать максимально активно. Дефолт зависит от платформы
memswap_limit Лимит на память плюс swap вместе. Работает только если задан mem_limit. Пример: mem_limit: 300m, memswap_limit: 1g — контейнеру достанется 300 МБ обычной памяти и до 700 МБ (1g минус 300m) свопа сверху. Не задали memswap_limit, но задали mem_limit? Тогда Docker по умолчанию выдаёт swap в том же объёме, что и сам лимит памяти. Значение 0 — игнорируется и считается незаданным. Значение, равное mem_limit — контейнер вообще не получает swap. -1 — swap без ограничений

Диск и устройства

Атрибут Что делает
blkio_config.weight Относительный приоритет контейнера в очереди на диск. Число от 10 до 1000, по умолчанию 500: чем больше, тем больше доля пропускной способности при конкуренции с другими контейнерами
blkio_config.weight_device То же самое, но отдельно для конкретного устройства: path плюс свой weight
blkio_config.device_read_bps / device_write_bps Жёсткий лимит скорости чтения или записи для конкретного устройства, в байтах в секунду
blkio_config.device_read_iops / device_write_iops То же самое, но лимит не на скорость, а на число операций в секунду
devices Прокидывает устройство хоста в контейнер: HOST_PATH:CONTAINER_PATH[:CGROUP_PERMISSIONS]. Либо CDI-синтаксис (vendor1.com/device=gpu), если за выбор устройства отвечает сам runtime
device_cgroup_rules Правила cgroup для устройств в формате, который понимает само ядро Linux (Device Whitelist Controller)
gpus Запрашивает GPU для контейнера: список объектов с driver и count, либо просто строка all, чтобы отдать все доступные GPU
storage_opt Опции storage-драйвера контейнера. Например, ограничение размера: size: '1G'

Пример блока I/O лимитов:

services:
  foo:
    image: busybox
    blkio_config:
      weight: 300
      device_read_bps:
        - path: /dev/sdb
          rate: '12mb'
services:
  foo:
    image: busybox
    blkio_config:
      weight: 300
      device_read_bps:
        - path: /dev/sdb
          rate: '12mb'

Здесь weight: 300 понижает приоритет контейнера в очереди на диск (дефолт 500, тут ниже). А device_read_bps работает отдельно от веса и жёстко: чтение именно с /dev/sdb не быстрее 12 МБ/с, неважно, какой у контейнера приоритет.

Capabilities и безопасность

Атрибут Что делает
privileged Запускает контейнер с повышенными привилегиями, по сути почти без изоляции от хоста. Конкретный эффект зависит от платформы
cap_add Добавляет конкретные Linux capabilities. Например: cap_add: [ALL]
cap_drop Убирает конкретные capabilities: cap_drop: [NET_ADMIN, SYS_ADMIN]
security_opt Переопределяет схему лейблов безопасности (SELinux/AppArmor). Булевы опции можно указывать без значения (no-new-privileges), с =true или :true — всё эквивалентно
group_add Добавляет пользователя внутри контейнера в дополнительные группы по имени или номеру. Пригождается, когда несколько контейнеров от разных пользователей пишут в один файл на общем томе: владельцем файла делают общую группу

Namespace и изоляция

Атрибут Что делает
ipc Режим изоляции IPC. shareable: свой приватный IPC namespace с возможностью поделиться им с другими контейнерами. service:{name}: присоединиться к IPC namespace другого сервиса вместо своего
pid В каком PID namespace запускать контейнер. Значения зависят от платформы
uts UTS namespace, то есть имя хоста и домена на уровне ядра. host означает, что контейнер использует тот же UTS namespace, что и хост
userns_mode Какой user namespace использовать для сервиса. Значения платформо-зависимы
cgroup В каком cgroup namespace запускать контейнер: host — в cgroup namespace самого движка, private — в своём собственном, изолированном
cgroup_parent Родительская cgroup для контейнера, если нужно поместить его в конкретное место иерархии cgroup
isolation Технология изоляции контейнера. Поддерживаемые значения платформо-зависимы, актуально в первую очередь для Windows

Прочие лимиты

Атрибут Что делает
pids_limit Максимум процессов и потоков (PID) внутри контейнера. -1 снимает лимит. Должен совпадать с deploy.resources.limits.pids, если задан и там
oom_kill_disable Запрещает платформе убивать именно этот контейнер при нехватке памяти на хосте
oom_score_adj Число от -1000 до 1000, которое влияет на то, выберет ли платформа этот контейнер для убийства при OOM. Чем больше число, тем выше шанс быть убитым первым
sysctls Меняет kernel-параметры внутри контейнера, но только namespaced — те, что не затрагивают хост целиком. Пример: net.core.somaxconn
ulimits Переопределяет ulimit для контейнера. Либо число для одного лимита, либо объект с soft и hard
shm_size Размер /dev/shm (shared memory) внутри контейнера, в байтовом формате
credential_spec Спецификация учётных данных managed service account для Windows-контейнеров. Варианты: file://..., registry://..., либо ссылка на конфиг через config
use_api_socket Даёт контейнеру доступ к API-сокету самого движка. Изнутри можно делать pull и push от тех же credentials, что и снаружи

Логирование

logging настраивает драйвер логирования для контейнеров сервиса:

logging:
  driver: syslog
  options:
    syslog-address: "tcp://192.168.0.42:123"
logging:
  driver: syslog
  options:
    syslog-address: "tcp://192.168.0.42:123"

driver — имя драйвера логирования. Дефолт и доступные значения зависят от платформы. options — опции драйвера в виде key-value пар.

Прочее: метаданные, расширение, модели

Атрибут Что делает
annotations Аннотации контейнера (массив или мапа)
attach false означает не собирать логи сервиса, пока не попросили явно
deploy Конфигурация развёртывания (раздел «deploy»)
develop Конфигурация для live-разработки (раздел «develop»)
models Какие AI-модели использует сервис (раздел «models»)
extends Наследование конфигурации сервиса из другого файла/сервиса
tty Выделить псевдо-TTY (true/false)
stdin_open Держать stdin открытым (аналог -i)

models на уровне сервиса:

services:
  short_syntax:
    image: app
    models:
      - my_model
  long_syntax:
    image: app
    models:
      my_model:
        endpoint_var: MODEL_URL
        model_var: MODEL
services:
  short_syntax:
    image: app
    models:
      - my_model
  long_syntax:
    image: app
    models:
      my_model:
        endpoint_var: MODEL_URL
        model_var: MODEL

Если endpoint_var/model_var не заданы, имена переменных генерируются автоматически: имя модели в верхнем регистре, - заменяется на _, плюс суффикс _URL.

extends — отдельная большая тема, потому что у него свои правила слияния, отличаются от обычного merge между файлами (см. раздел «merge»):

extends:
  file: common.yml
  service: webapp
extends:
  file: common.yml
  service: webapp
  • Если file не указан, берётся сервис из текущего файла.
  • Циклические ссылки запрещены, Compose вернёт ошибку.
  • При extends ресурсы (volumes, networks, configs, secrets, links, depends_on и т.п.), которые использует наследуемый сервис, не подтягиваются автоматически, их нужно объявить в файле, который наследует. Правила слияния при extends (своя, отдельная от общего merge-механизма логика):
Тип значения Как сливается
Мапы (environment, labels, healthcheck, sysctls, ulimits, build.args и т.п.) Ключи текущего сервиса перекрывают ключи из наследуемого, остальное сохраняется
volumes, devices, blkio_config.device_* Считаются мапами по ключу, в качестве ключа берутся пути назначения внутри контейнера
Списки (cap_add, cap_drop, configs, ports, secrets, expose, security_opt и пр.) Элементы объединяются, дубликаты удаляются
Списки dns, dns_search, env_file, tmpfs (если задан в виде списка) Объединяются, дубликаты не удаляются
Скаляры Значение текущего сервиса побеждает

Отдельный нюанс с healthcheck: текущий сервис не может выставить disable: true, если наследуемый сервис этого не делает, в таком случае Compose вернёт ошибку.

build — как Compose собирает образ

Кому и зачем: если у вас уже есть готовый образ из реестра (Docker Hub и т.п.) — раздел можно пропустить. Нужен, только если вы собираете образ из исходников через Compose.

build можно задать строкой (путь к контексту сборки) или объектом с детальными настройками. Если задана строка, в этой папке Compose будет искать Dockerfile. Относительный путь резолвится от директории проекта, абсолютный — работает, но Compose выдаст предупреждение о непортируемости файла.

services:
  webapp:
    build: ./dir              # context = ./dir, Dockerfile внутри обязателен
 
  webapp2:
    build: https://github.com/mycompany/example.git#branch_or_tag:subdirectory
services:
  webapp:
    build: ./dir              # context = ./dir, Dockerfile внутри обязателен
 
  webapp2:
    build: https://github.com/mycompany/example.git#branch_or_tag:subdirectory

Если у сервиса заданы и build, и image, поведение регулируется pull_policy: по умолчанию Compose сперва пытается запулить образ, и только если не нашёл, собирает из исходников.

Атрибут build.* Что делает Пример
context Путь к директории с Dockerfile или Git URL. По умолчанию . (директория проекта). Абсолютный путь — предупреждение о непортируемости context: ./dir
dockerfile Альтернативный путь к Dockerfile относительно контекста dockerfile: webapp.Dockerfile
dockerfile_inline Содержимое Dockerfile прямо в compose-файле (несовместимо с dockerfile) dockerfile_inline: FROM baseimage ...
args Build-аргументы (ARG из Dockerfile), мапа или список args: {GIT_COMMIT: cdc3b19}
additional_contexts Доп. именованные контексты для сборки. Поддерживает пути, Git URL, ссылки на образы (docker-image://my-app:latest) и образы других сервисов (service:name) additional_contexts: {base: service:base}
cache_from / cache_to Источники/назначения кэша сборки, формат [NAME type=TYPE[,KEY=VALUE]] cache_from: [alpine:latest, type=gha]
target Стадия в multi-stage Dockerfile target: prod
network Сеть для RUN-инструкций во время сборки, либо none network: host
platforms Список целевых платформ образа. Если не задан — Compose включает платформу сервиса автоматически. Ошибка, если список не пустой, но не содержит платформу сервиса ["linux/amd64", "linux/arm64"]
pull Принудительно пуллить базовые образы (FROM), даже если они в локальном кэше pull: true
no_cache Полная пересборка без кэша builder’а. Применяется только к слоям из Dockerfile; referenced images всё равно могут браться из локального стора (для их обновления используйте pull: true) no_cache: true
privileged Сборка с повышенными привилегиями privileged: true
isolation Технология изоляции контейнера сборки платформо-зависимо
labels Метаданные на итоговом образе мапа или список
shm_size Размер shared memory при сборке shm_size: "2gb"
ssh SSH-доступ для сборки (например, клонирование приватного репо) ssh: [default] или ssh: [myproject=~/.ssh/key.pem]
secrets Доступ к секретам только во время сборки см. ниже
tags Дополнительные теги для образа, в дополнение к image ["myimage:mytag"]
ulimits ulimit’ы для контейнера сборки как в services.ulimits
extra_hosts Доп. записи hosts во время сборки как в services.extra_hosts
entitlements Доп. привилегированные права для сборки [network.host, security.insecure]
provenance Provenance attestation для образа (bool или mode=...) provenance: mode=max
sbom SBOM attestation (bool или generator=...) sbom: true

Секреты при сборке доступны только в момент build и работают иначе, чем services.secrets. В длинном синтаксисе атрибут target — это ID секрета в Dockerfile (тот самый id= в RUN --mount=type=secret):

services:
  frontend:
    build:
      context: .
      secrets:
        - source: server-certificate
          target: cert           # это id для --mount=type=secret,id=cert
          uid: "103"
          gid: "103"
          mode: 0440
secrets:
  server-certificate:
    external: true
services:
  frontend:
    build:
      context: .
      secrets:
        - source: server-certificate
          target: cert           # это id для --mount=type=secret,id=cert
          uid: "103"
          gid: "103"
          mode: 0440
secrets:
  server-certificate:
    external: true
# Dockerfile
FROM nginx
RUN --mount=type=secret,id=cert,required=true,target=/root/cert ...
# Dockerfile
FROM nginx
RUN --mount=type=secret,id=cert,required=true,target=/root/cert ...

Если у образа нет атрибута image, при пуше Compose пропускает его с предупреждением: пушить туда, по сути, нечего.

deploy — параметры развёртывания

Кому и зачем: если вы запускаете Compose на одной машине без Swarm/Kubernetes — большая часть раздела не применяется. Актуально для кластерных развёртываний с репликами, лимитами ресурсов и placement-констрейнтами.

deploy — это опциональная секция про то, как платформа должна запускать и масштабировать сервис. Если платформа не умеет в Deploy Spec, секция просто игнорируется, файл всё равно валиден.

Атрибут Что делает
mode Модель репликации: replicated (по умолчанию), global (1 задача на ноду), replicated-job (N задач до успешного завершения), global-job (1 задача на ноду до успешного завершения; автоматически запускается на новых нодах по мере их добавления)
replicas Сколько контейнеров держать запущенными при mode: replicated
endpoint_mode vip (виртуальный IP, балансировка платформой) или dnsrr (DNS round-robin)
labels Метаданные на самом сервисе (не на контейнерах)
placement.constraints Жёсткие требования к ноде (node.labels.disktype==ssd)
placement.preferences Стратегия распределения задач, пока только spread
resources.limits / resources.reservations Максимум / гарантированный минимум ресурсов
restart_policy Условия и параметры перезапуска контейнеров. Если не задан — Compose смотрит на restart из service-конфигурации как fallback
update_config Как накатывать обновления (rolling update)
rollback_config Как откатывать неудачное обновление
services:
  frontend:
    image: example/webapp
    deploy:
      mode: replicated
      replicas: 2
      endpoint_mode: vip
      placement:
        constraints:
          - node.labels.disktype==ssd
        preferences:
          - spread: node.labels.zone
      resources:
        limits:
          cpus: '0.50'
          memory: 50M
          pids: 1
        reservations:
          cpus: '0.25'
          memory: 20M
          devices:
            - capabilities: ["gpu"]
              count: 2
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
        window: 120s
      update_config:
        parallelism: 2
        delay: 10s
        order: stop-first
services:
  frontend:
    image: example/webapp
    deploy:
      mode: replicated
      replicas: 2
      endpoint_mode: vip
      placement:
        constraints:
          - node.labels.disktype==ssd
        preferences:
          - spread: node.labels.zone
      resources:
        limits:
          cpus: '0.50'
          memory: 50M
          pids: 1
        reservations:
          cpus: '0.25'
          memory: 20M
          devices:
            - capabilities: ["gpu"]
              count: 2
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
        window: 120s
      update_config:
        parallelism: 2
        delay: 10s
        order: stop-first

Что тут настроено, по шагам: Compose держит 2 реплики сервиса (replicas: 2) с общим виртуальным IP на всех (endpoint_mode: vip), запускает их только на нодах с SSD (constraints) и старается равномерно раскидать реплики по зонам (preferences). Каждому контейнеру разрешено не больше половины ядра CPU и 50 МБ памяти, а гарантированно выделено четверть ядра и 20 МБ. При падении контейнер перезапускается с паузой 5 секунд между попытками, но не больше 3 раз. А когда сервис обновляется, контейнеры пересоздаются по 2 штуки за раз, и старая версия останавливается перед запуском новой (stop-first), а не одновременно с ней.

Параметры resources.*.devices (резервирование устройств типа GPU/TPU):

Атрибут Что делает
capabilities Обязательный список возможностей: gpu, tpu, либо специфичные для драйвера (с префиксом, например nvidia-compute)
driver Какой драйвер использовать для устройства
count Сколько устройств зарезервировать. Если не задан или задан как all — резервируются все подходящие устройства. Взаимоисключимо с device_ids
device_ids Конкретные ID устройств (взаимоисключимо с count)
options Опции драйвера в виде key-value

restart_policy:

Атрибут Значение по умолчанию
condition any — перезапускать всегда; on-failure — только при ненулевом коде; none — никогда
delay 0 — задержка между попытками
max_attempts без ограничений. Важный нюанс: неудачная попытка засчитывается только если контейнер не поднялся успешно в течение window. То есть при max_attempts: 2 Compose может физически попробовать больше двух раз, пока не накопится 2 засчитанных провала
window 0 — оценивать успех сразу

update_config / rollback_config — одинаковый набор полей: parallelism, delay, failure_action (continue/rollback/pause для update, continue/pause для rollback), monitor, max_failure_ratio, order (stop-first/start-first).

Важно: job-режимы (replicated-job, global-job) рассчитаны на задачи, которые завершаются с кодом 0. Завершённые задачи остаются, пока их явно не удалят. max-concurrent для них настраивается только через CLI, в Compose-файле такого параметра нет.

develop — режим разработки (watch)

Кому и зачем: если вы не используете live-разработку (автоматическую пересборку при изменении файлов) — можно пропустить. Полезно, когда вы активно итерируете код и хотите, чтобы Compose сам подхватывал изменения.

develop — опциональная секция, появилась в Compose 2.22.0, нужна для “внутреннего цикла” разработки: следить за файлами и реагировать на изменения без полного пересоздания всего стека руками.

services:
  frontend:
    image: example/webapp
    build: ./webapp
    develop:
      watch:
        - path: ./webapp/html
          action: sync
          target: /var/www
          ignore:
            - node_modules/
 
  backend:
    image: example/backend
    build: ./backend
    develop:
      watch:
        - path: ./backend/src
          action: rebuild
services:
  frontend:
    image: example/webapp
    build: ./webapp
    develop:
      watch:
        - path: ./webapp/html
          action: sync
          target: /var/www
          ignore:
            - node_modules/
 
  backend:
    image: example/backend
    build: ./backend
    develop:
      watch:
        - path: ./backend/src
          action: rebuild

В этом примере у frontend действие sync: при изменении файлов в ./webapp/html Compose просто копирует их внутрь работающего контейнера по пути /var/www, не трогая сам контейнер (кроме папки node_modules, её игнорируем). У backend действие rebuild: при изменении файлов в ./backend/src Compose пересобирает образ заново и пересоздаёт контейнер с нуля — дольше, но нужно, когда правки требуют пересборки (например, меняется зависимость).

Атрибуты внутри каждого правила watch:

Атрибут Что делает
path Путь (относительно проекта), который мониторится
action Что делать при изменении: rebuild, restart (с 2.32.0), sync, sync+restart (с 2.23.0), sync+exec (с 2.32.0)
target Куда внутри контейнера синхронизировать файлы (только для sync-действий)
ignore Паттерны путей, которые игнорируются (синтаксис как у .dockerignore). Если в build-контексте есть .dockerignore, его паттерны загружаются как implicit content, а паттерны из Compose-модели добавляются к ним
include Паттерны путей, которые наоборот включаются в отслеживание (удобно вместо длинного ignore)
initial_sync Проверять при старте watch-сессии, что файлы в уже существующем контейнере синхронизированы
exec Команда, которая выполняется внутри контейнера при action: sync+exec

exec — те же поля, что у lifecycle-хуков (command, user, privileged, working_dir, environment):

services:
  frontend:
    develop:
      watch:
        - path: ./etc/config
          action: sync+exec
          target: /etc/config/
          exec:
            command: app reload
services:
  frontend:
    develop:
      watch:
        - path: ./etc/config
          action: sync+exec
          target: /etc/config/
          exec:
            command: app reload

Если include начинается с *, обязательно берите паттерн в кавычки — иначе YAML примет звёздочку за alias-node.

networks — именованные сети

Кому и зачем: всем, у кого больше одного сервиса. Если сервисы должны общаться друг с другом по именам, а не через localhost, этот раздел нужен.

Top-level networks позволяет объявить сети, которые можно переиспользовать между сервисами (подключение к сети на уровне сервиса всё равно нужно делать явно через services.<name>.networks).

services:
  proxy:
    build: ./proxy
    networks:
      - frontend
  app:
    build: ./app
    networks:
      - frontend
      - backend
  db:
    image: postgres:18
    networks:
      - backend
 
networks:
  frontend:
    driver: bridge
    driver_opts:
      com.docker.network.bridge.host_binding_ipv4: "127.0.0.1"
  backend:
    driver: custom-driver
services:
  proxy:
    build: ./proxy
    networks:
      - frontend
  app:
    build: ./app
    networks:
      - frontend
      - backend
  db:
    image: postgres:18
    networks:
      - backend
 
networks:
  frontend:
    driver: bridge
    driver_opts:
      com.docker.network.bridge.host_binding_ipv4: "127.0.0.1"
  backend:
    driver: custom-driver

В этом примере proxy и db изолированы друг от друга, потому что не делят общую сеть — общаться напрямую может только app.

Атрибут Что делает
driver Драйвер сети, ошибка если недоступен на платформе
driver_opts Опции драйвера, key-value
attachable Разрешить отдельным (standalone) контейнерам подключаться к сети
enable_ipv4 / enable_ipv6 Включить/выключить выдачу IPv4/IPv6 адресов. enable_ipv4: false удобен, когда нужна сеть только с IPv6
internal Изолировать сеть от внешнего мира (по умолчанию Compose даёт внешнюю связность)
ipam Кастомная IPAM-конфигурация: driver, config (subnet/ip_range/gateway/aux_addresses), options
labels Метаданные сети (массив или мапа). Compose также автоматически проставляет com.docker.compose.project и com.docker.compose.network
name Кастомное имя сети без привязки к имени проекта
external Сеть уже существует и управляется не Compose; все прочие атрибуты, кроме name, недопустимы

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

networks:
  default:
    name: a_network
    driver_opts:
      com.docker.network.bridge.host_binding_ipv4: 127.0.0.1
networks:
  default:
    name: a_network
    driver_opts:
      com.docker.network.bridge.host_binding_ipv4: 127.0.0.1

Внешняя сеть — по аналогии с внешними томами:

networks:
  outside:
    external: true
networks:
  outside:
    external: true

volumes верхнего уровня

Кому и зачем: если вы используете named volumes (а не только bind mount) — нужно. Без top-level объявления Compose не будет знать, что такой том существует.

Top-level 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:
services:
  backend:
    image: example/database
    volumes:
      - db-data:/etc/data
  backup:
    image: backup-service
    volumes:
      - db-data:/var/lib/backup/data
 
volumes:
  db-data:

Оба сервиса смотрят в один и тот же том db-data, но по разным путям внутри своих контейнеров: backend пишет туда данные базы (/etc/data), а backup видит те же файлы по пути /var/lib/backup/data — то есть может забрать и заархивировать их, не трогая контейнер с самой базой.

docker compose up создаёт том, если он ещё не существует. Если том уже есть — используется существующий. Если том был удалён вручную вне Compose — пересоздаётся.

Атрибут Что делает
driver Драйвер тома, ошибка если недоступен
driver_opts Опции драйвера. Через driver: local + driver_opts (type: none, o: bind, device: /абсолютный/путь) делают “именованный bind mount” — стабильное имя тома, который физически указывает на конкретную папку хоста
external Том уже существует, Compose его не создаёт; прочие атрибуты кроме name недопустимы
labels Метаданные тома. Применяются только к именованным томам, не к bind mount; видны через docker volume inspect. Compose также автоматически проставляет com.docker.compose.project и com.docker.compose.volume
name Кастомное имя тома без скоупа по имени стека

Пример внешнего тома с параметризованным именем для поиска (имя в файле фиксировано, а реальное имя на платформе задаётся через переменную):

volumes:
  db-data:
    external: true
    name: actual-name-of-volume
volumes:
  db-data:
    external: true
    name: actual-name-of-volume

Пустая запись (db-data: без атрибутов) — это том с настройками движка по умолчанию.

configs и secrets

Кому и зачем: если вы передаёте конфиги и секреты через обычные environment или volumes (bind mount) — можно пропустить. Этот раздел для более структурированного подхода через специализированные top-level секции.

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

configs secrets
Источники file, environment, content, external file, environment
Куда монтируется по умолчанию /<config-name> (Linux) / C:\<config-name> (Windows) /run/secrets/<secret-name>
Права по умолчанию мир-readable, 0444 мир-readable, 0444
environment как источник поддерживается docker stack deploy? Нет, только обычный Compose. Для stack deploy используйте file или external
name для внешнего ресурса поддерживается поддерживается

Все четыре варианта источника для configs:

configs:
  http_config:
    file: ./httpd.conf          # 1. из файла
 
  http_config_ext:
    external: true              # 2. уже существует на платформе
 
  app_config:
    content: |                  # 3. инлайн-контент, с интерполяцией переменных
      debug=${DEBUG}
      spring.application.name=${COMPOSE_PROJECT_NAME}
 
  simple_config:
    environment: "SIMPLE_CONFIG_VALUE"   # 4. из переменной окружения хоста
configs:
  http_config:
    file: ./httpd.conf          # 1. из файла
 
  http_config_ext:
    external: true              # 2. уже существует на платформе
 
  app_config:
    content: |                  # 3. инлайн-контент, с интерполяцией переменных
      debug=${DEBUG}
      spring.application.name=${COMPOSE_PROJECT_NAME}
 
  simple_config:
    environment: "SIMPLE_CONFIG_VALUE"   # 4. из переменной окружения хоста

secrets — только file и environment:

secrets:
  server-certificate:
    file: ./server.cert
  token:
    environment: "OAUTH_TOKEN"
secrets:
  server-certificate:
    file: ./server.cert
  token:
    environment: "OAUTH_TOKEN"

При деплое <project_name>_http_config и <project_name>_server-certificate создаются автоматически. Если external: true — все прочие атрибуты, кроме name, под запретом, Compose отклонит файл как невалидный, если найдёт что-то ещё.

Поиск внешнего ресурса под другим именем (удобно, когда имя ключа известно заранее, а реальный ID подставляется при деплое):

configs:
  http_config:
    external: true
    name: "${HTTP_CONFIG_KEY}"
configs:
  http_config:
    external: true
    name: "${HTTP_CONFIG_KEY}"

models — AI-модели в Compose

Кому и зачем: только если вы используете AI-модели через Compose runner. Для обычных проектов без ML — раздел не нужен.

Top-level models описывает AI-модели, которые Compose пуллит как OCI-артефакты, запускает через model runner и отдаёт сервисам как API.

services:
  app:
    image: app
    models:
      - ai_model
 
models:
  ai_model:
    model: ai/model
services:
  app:
    image: app
    models:
      - ai_model
 
models:
  ai_model:
    model: ai/model

Сервис app получает доступ к модели, а Compose сам прокидывает в контейнер переменную с адресом, например AI_MODEL_URL.

Атрибут Что делает
model Обязательный. Идентификатор OCI-артефакта модели, который пуллится и запускается
context_size Максимальный размер контекста (в токенах)
runtime_flags Список сырых флагов командной строки для движка инференса

Длинный синтаксис на уровне сервиса (с явным именем переменной) уже был в разделе 3.7 — endpoint_var / model_var.

models:
  my_model:
    model: ai/model
    context_size: 1024
    runtime_flags:
      - "--a-flag"
      - "--another-flag=42"
models:
  my_model:
    model: ai/model
    context_size: 1024
    runtime_flags:
      - "--a-flag"
      - "--another-flag=42"

profiles — включаем нужные сервисы

Кому и зачем: если у вас один compose-файл, но разные сервисы для dev/prod/test — profiles позволяют включать только нужные. Если всё запускается всегда — можно пропустить.

profiles позволяет держать в одном файле сервисы для разных сценариев (тесты, дебаг, прод) и включать только нужные. Сервис без profiles всегда активен. Если ни один профиль сервиса не совпал с активными — сервис игнорируется, если только его не запросили явно командой (тогда его профиль активируется автоматически).

services:
  web:
    image: web_image
 
  test_lib:
    image: test_lib_image
    profiles: [test]
 
  coverage_lib:
    image: coverage_lib_image
    depends_on:
      - test_lib
    profiles: [test]
 
  debug_lib:
    image: debug_lib_image
    depends_on:
      - test_lib
    profiles: [debug]
services:
  web:
    image: web_image
 
  test_lib:
    image: test_lib_image
    profiles: [test]
 
  coverage_lib:
    image: coverage_lib_image
    depends_on:
      - test_lib
    profiles: [test]
 
  debug_lib:
    image: debug_lib_image
    depends_on:
      - test_lib
    profiles: [debug]
Сценарий запуска Какие сервисы в модели
Без активных профилей только web
Профиль test web, test_lib, coverage_lib
Профиль debug web, debug_lib — но модель невалидна: debug_lib зависит от test_lib, а у него нет общего профиля с debug_lib
Профили test и debug вместе все четыре сервиса
Явный запуск coverage_lib активируется профиль test, test_lib подключается как зависимость
Явный запуск debug_lib без профиля test ошибка — зависимость test_lib не подходит по профилю
Явный запуск debug_lib + активный профиль test профиль debug включается автоматически, test_lib тоже стартует

Важно: ссылки на другие сервисы через links, extends или синтаксис service:xxx не включают автоматически отключенный профилем сервис — в этом случае Compose вернёт ошибку, а не подключит сервис “по умолчанию”.

include — модульные compose-файлы

Кому и зачем: если ваш compose-файл стал огромным и вы хотите разбить его на части — этот раздел для вас. Для небольших проектов — не нужен.

include нужен, чтобы выносить часть модели приложения в отдельные файлы и подключать их — для переиспользования, или когда разные команды должны видеть только свою часть. Каждый подключённый файл загружается как отдельная Compose-модель со своей собственной project directory (относительные пути внутри него считаются от его собственной папки, а не от вашей). Конфликты имён ресурсов Compose не сливает — только предупреждает.

include:
  - my-compose-include.yaml
services:
  serviceA:
    build: .
    depends_on:
      - serviceB   # объявлен в подключённом файле, но доступен как свой
include:
  - my-compose-include.yaml
services:
  serviceA:
    build: .
    depends_on:
      - serviceB   # объявлен в подключённом файле, но доступен как свой

Короткий синтаксис — просто список путей:

include:
  - ../commons/compose.yaml
  - ../another_domain/compose.yaml
include:
  - ../commons/compose.yaml
  - ../another_domain/compose.yaml

Длинный синтаксис добавляет контроль над тем, как парсится подпроект:

include:
  - path: ../commons/compose.yaml
    project_directory: ..
    env_file: ../another/.env
include:
  - path: ../commons/compose.yaml
    project_directory: ..
    env_file: ../another/.env
Атрибут Что делает
path Обязательный. Путь к файлу, либо список путей, если несколько файлов нужно слить в один подпроект
project_directory Базовая папка для относительных путей внутри включаемого файла, по умолчанию — папка самого файла
env_file .env-файл(ы) со значениями по умолчанию для интерполяции в подключаемом файле. По умолчанию ищется .env в project_directory включаемого файла. Принимает строку или список строк, если нужно смержить несколько env-файлов

Переменные окружения локального проекта имеют приоритет над значениями из env_file подключённого файла — то есть переопределить подпроект “снаружи” можно. include работает рекурсивно: если подключённый файл сам что-то include-ит, эти файлы подключатся тоже. И поддерживается интерполяция прямо в пути:

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

Extensions (x-) и YAML-фрагменты

Кому и зачем: если вы хотите переиспользовать куски YAML через якоря (&/*/<<:) или добавить кастомные поля, которые Compose игнорирует — раздел полезен. Для простых файлов — можно пропустить.

Это два разных механизма с одной целью — не повторять одно и то же по десять раз в файле.

Extensions — любое поле, начинающееся с x-. Это единственное место, где Compose молча игнорирует неизвестное поле, причём работает на любом уровне вложенности, включая платформоспецифичные расширения внутри стандартных секций. Исторически сложившиеся вендорские префиксы: docker (Docker), kubernetes (Kubernetes).

x-custom:
  foo: [bar, zot]
 
services:
  webapp:
    image: example/webapp
    x-foo: bar
x-custom:
  foo: [bar, zot]
 
services:
  webapp:
    image: example/webapp
    x-foo: bar
service:
  backend:
    deploy:
      placement:
        x-aws-role: "arn:aws:iam::XXXXXXXXXXXX:role/foo"
        x-aws-region: "eu-west-3"
service:
  backend:
    deploy:
      placement:
        x-aws-role: "arn:aws:iam::XXXXXXXXXXXX:role/foo"
        x-aws-region: "eu-west-3"

Фрагменты — это обычный YAML-механизм анкоров (&имя) и алиасов (*имя), без всякого отношения к Compose как таковому. Анкор резолвится раньше, чем подставляются переменные (${VAR}), поэтому переменными нельзя управлять самими анкорами/алиасами. Анкор можно поставить прямо на поле внутри сервиса, не только в x--блоке:

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

Анкоры особенно хорошо работают в связке с x--расширениями, чтобы общий блок не “принадлежал” ни одному сервису:

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

Частичное переопределение через YAML merge (<<:) — взять анкор, но поменять конкретное поле:

volumes:
  db-data: &default-volume
    driver: default
    name: "data"
  metrics:
    <<: *default-volume
    name: "metrics"
volumes:
  db-data: &default-volume
    driver: default
    name: "data"
  metrics:
    <<: *default-volume
    name: "metrics"

Несколько анкоров сразу — <<: [*a, *b]:

x-environment: &default-environment
  FOO: BAR
x-keys: &keys
  KEY: VALUE
services:
  frontend:
    environment:
      <<: [*default-environment, *keys]
      YET_ANOTHER: VARIABLE
x-environment: &default-environment
  FOO: BAR
x-keys: &keys
  KEY: VALUE
services:
  frontend:
    environment:
      <<: [*default-environment, *keys]
      YET_ANOTHER: VARIABLE

YAML merge (<<:) работает только с мапами. Если используете список переменных окружения вида - FOO=BAR, фрагменты в этом виде не сработают — нужна именно мап-форма FOO: BAR.

И раз уж заговорили про extension.md — там же, на правах справочника, описаны два формата значений, которые используются по всему Compose-файлу:

Байтовые значения ({amount}{unit}, единицы b, k/kb, m/mb, g/gb):

2b
1024kb
2048k
300m
1gb
2b
1024kb
2048k
300m
1gb

Длительности ({value}{unit}, единицы us, ms, s, m, h, можно комбинировать без разделителя):

10ms
40s
1m30s
1h5m30s20ms
10ms
40s
1m30s
1h5m30s20ms

interpolation — подстановка переменных

Кому и зачем: всем, кто использует .env-файлы или переменные окружения в compose-файле. Если у вас нет ни одной ${VAR} — можно пропустить.

Compose поддерживает Bash-подобный синтаксис подстановки переменных: $VAR и ${VAR} равнозначны, но у фигурных скобок есть дополнительные формы. Важно: Compose обрабатывает строку после $ только если она образует валидное имя переменной — либо [_a-zA-Z][_a-zA-Z0-9]*, либо ${...}. В остальных случаях строка сохраняется как есть.

Интерполяция применяется до merge, на уровне каждого файла отдельно.

Форма Что делает
${VAR} Прямая подстановка значения
${VAR:-default} default, если VAR не задана или пустая
${VAR-default} default, только если VAR не задана вообще (пустая строка — это всё ещё значение)
${VAR:?error} Завершить с ошибкой, если VAR не задана или пустая
${VAR?error} Завершить с ошибкой, только если VAR совсем не задана
${VAR:+replacement} replacement, если VAR задана и не пустая, иначе пустая строка
${VAR+replacement} replacement, если VAR задана (даже пустым значением)

Подстановки можно вкладывать друг в друга: ${VARIABLE:-${FOO:-default}}.

Если переменная не резолвится и default не задан — Compose выводит предупреждение и подставляет пустую строку. Расширенные shell-фичи типа ${VARIABLE/foo/bar} не поддерживаются.

Чтобы получить буквальный знак доллара и не дать Compose его интерпретировать — $$:

web:
  command: "$$VAR_NOT_INTERPOLATED_BY_COMPOSE"
web:
  command: "$$VAR_NOT_INTERPOLATED_BY_COMPOSE"

Отдельный нюанс: интерполяция применяется только к значениям, не к ключам. Если ключ — произвольная пользовательская строка (например, в labels или environment), для интерполяции ключа нужен альтернативный синтаксис со знаком =:

services:
  foo:
    labels:
      "$VAR_NOT_INTERPOLATED_BY_COMPOSE": "BAR"   # ключ — как есть
      
services:
  foo:
    labels:
      - "$VAR_INTERPOLATED_BY_COMPOSE=BAR"        # а здесь сработает
services:
  foo:
    labels:
      "$VAR_NOT_INTERPOLATED_BY_COMPOSE": "BAR"   # ключ — как есть
      
services:
  foo:
    labels:
      - "$VAR_INTERPOLATED_BY_COMPOSE=BAR"        # а здесь сработает

merge — слияние нескольких compose-файлов

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

Когда модель приложения собирается из нескольких файлов (например, compose.yaml + compose.override.yaml), Compose сливает их по понятным правилам, плюс пара спецтегов для ручного управления.

Тип данных Правило
Мапа (mapping) Недостающие ключи добавляются, общие — рекурсивно сливаются
Список (sequence) Значения из второго файла добавляются к значениям из первого
# файл 1                    # файл 2
services:                   services:
  foo:                        foo:
    key1: value1                 key2: VALUE
    key2: value2                 key3: value3
# файл 1                    # файл 2
services:                   services:
  foo:                        foo:
    key1: value1                 key2: VALUE
    key2: value2                 key3: value3

→ результат: key1: value1, key2: VALUE, key3: value3.

Но есть исключения из этих двух правил:

Что Правило
command, entrypoint, healthcheck.test Не складываются, а полностью перезаписываются последним файлом
volumes, secrets, configs (уникальный ключ — target), ports (уникальный ключ — {ip, target, published, protocol}) Хотя формально это списки, Compose считает их по уникальному ключу: новые записи добавляются, совпадающие по ключу — сливаются как мапы

Пример с уникальным ключом для volumes (совпал target: /work — значит, это “тот же” элемент, второй файл выигрывает):

# файл 1: volumes: [foo:/work]
# файл 2: volumes: [bar:/work]
# результат: volumes: [bar:/work]
# файл 1: volumes: [foo:/work]
# файл 2: volumes: [bar:/work]
# результат: volumes: [bar:/work]

Два спецтега YAML для ручного управления слиянием:

!reset — стереть значение, заданное предыдущим файлом (тип сохраняется как default/null, конкретное значение после тега не важно, но для читаемости лучше явно писать null или []):

# compose.yaml
services:
  app:
    image: myapp
    ports: ["8080:80"]
    environment:
      FOO: BAR
 
# compose.override.yaml
services:
  app:
    ports: !reset []
    environment:
      FOO: !reset null
 
# результат
services:
  app:
    image: myapp
# compose.yaml
services:
  app:
    image: myapp
    ports: ["8080:80"]
    environment:
      FOO: BAR
 
# compose.override.yaml
services:
  app:
    ports: !reset []
    environment:
      FOO: !reset null
 
# результат
services:
  app:
    image: myapp

!override — полностью заменить значение, игнорируя обычные правила слияния (актуально для ports/volumes/secrets/configs, которые иначе слились бы по уникальному ключу, а не заменились целиком):

# compose.yaml: ports: ["8080:80"]
# compose.override.yaml:
services:
  app:
    ports: !override
      - "8443:443"
 
# результат: ports: ["8443:443"]
# без !override получили бы оба порта одновременно
# compose.yaml: ports: ["8080:80"]
# compose.override.yaml:
services:
  app:
    ports: !override
      - "8443:443"
 
# результат: ports: ["8443:443"]
# без !override получили бы оба порта одновременно

Заключение

Если выбросить из головы все таблицы, смысл главы простой: compose.yaml — это не один большой плоский список настроек, а несколько независимых top-level разделов (services, networks, volumes, configs, secrets, models, include, x-*), которые ссылаются друг на друга по имени. Сервис сам по себе — самый объёмный раздел, потому что в нём собрано почти всё: какой образ запускать, как его собрать (build), как развернуть (deploy), как разрабатывать (develop), к каким сетям/томам/секретам подключить.

Отдельно стоит держать в голове три механики, которые работают сквозь весь файл и легко забываются: интерполяция переменных (${VAR} и её формы с default/required/alternative — применяется до merge, на уровне каждого файла отдельно), правила merge при работе с несколькими файлами (обычный merge ≠ правила extends, и для обоих есть исключения вроде command/healthcheck.test или уникальных ключей у volumes/ports), и YAML-анкоры — они подставляются раньше, чем интерполяция переменных, так что переменными анкоры не настроить.

Теперь, когда структура самого файла понятна, в следующей части пойду разбирать сетевую модель Docker подробнее — благо top-level networks и атрибуты подключения на уровне сервиса я здесь уже показал.