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

в 9:01, , рубрики: docker, Docker Hub, docker-compose, dockerhub, ruvds_статьи
Docker Fundamentals: что внутри compose.yaml и как там всё устроено - 1

В прошлой части я разобрал хранилища — тома, 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 # comment → VAL

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 ООО «МТ ФИНАНС»

Автор: opensophy

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js