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

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

1. Из чего состоит 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 разделы всегда активны, независимо от того, какие профили включены.

2. 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}"

3. services

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

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

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

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

Атрибут

Что делает

Пример / примечание

image

Образ для запуска контейнера, формат [registry/][project/]image[:tag|@digest]

image: redis:5

build

Конфигурация сборки образа из исходников (см. раздел 4)

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

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

  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 снести то, что он создал.

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

Атрибут

Что делает

restart

Политика перезапуска: no (по умолчанию), always, on-failure[:max-retries], unless-stopped

healthcheck

Проверка “здоровья” контейнера, переопределяет HEALTHCHECK из образа

depends_on

Порядок запуска/остановки сервисов

profiles

Список профилей, при которых сервис активен (см. раздел 11)

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

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.

Параметр длинного синтаксиса

Значение

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

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

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

Атрибут

Что делает

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"

Поясню пару строк, чтоб было понятно. "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

expose:

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"

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

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

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

Атрибут

Что делает

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

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"

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

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

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, и в environment — побеждает environment, даже если значение пустое.

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

  • Строки начиная с # — комментарии, игнорируются

  • Разделитель между ключом и значением — = или :

  • Значения в двойных кавычках поддерживают escape-последовательности: \n, \t, \\

  • Значения в одинарных кавычках берутся буквально: VAR='$OTHER'$OTHER

  • Инлайновый комментарий для незакавыченных значений нужно предварять пробелом: VAR=VAL # commentVAL

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

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

tmpfs:

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.

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

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'

Здесь 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, что и снаружи

3.6 Логирование

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

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

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

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

Атрибут

Что делает

annotations

Аннотации контейнера (массив или мапа)

attach

false означает не собирать логи сервиса, пока не попросили явно

deploy

Конфигурация развёртывания (раздел 5)

develop

Конфигурация для live-разработки (раздел 6)

models

Какие AI-модели использует сервис (раздел 10)

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

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

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

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 вернёт ошибку.

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

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

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
# Dockerfile
FROM nginx
RUN --mount=type=secret,id=cert,required=true,target=/root/cert ...

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

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

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

Что тут настроено, по шагам: 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-файле такого параметра нет.

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

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

В этом примере у 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

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

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

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

В этом примере 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:
  outside:
    external: true

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

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:

Оба сервиса смотрят в один и тот же том 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

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

9. configs и secrets

Эти два раздела почти близнецы: оба монтируют данные файлами в контейнер, оба требуют явного разрешения на уровне сервиса. Разница — 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. из переменной окружения хоста

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

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}"

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

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

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"

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

Сценарий запуска

Какие сервисы в модели

Без активных профилей

только 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 вернёт ошибку, а не подключит сервис “по умолчанию”.

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

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

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

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

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

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

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

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

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

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

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"

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

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

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

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

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

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

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

2b
1024kb
2048k
300m
1gb

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

10ms
40s
1m30s
1h5m30s20ms

14. interpolation — переменные ${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"

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

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

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

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

Тип данных

Правило

Мапа (mapping)

Недостающие ключи добавляются, общие — рекурсивно сливаются

Список (sequence)

Значения из второго файла добавляются к значениям из первого

# файл 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]

Два спецтега 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

!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 — это не один большой плоский список настроек, а несколько независимых 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 и атрибуты подключения на уровне сервиса я здесь уже показал.

© 2026 ООО «МТ ФИНАНС»

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


  1. Joshuya
    24.07.2026 10:21

    Thank you for the article! Could you please tell me how I can download the tables from it? I'd like to combine them all into a single cheat sheet for easy reference. Unfortunately, they don't copy properly on my end. :(