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

Докер

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

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


Введение

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

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

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

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

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

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

version и name

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

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

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

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

services

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

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

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

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

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

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

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

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

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

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

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

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

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

healthcheck пример:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

expose:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

services:
  one:
    label_file: ./app.labels

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

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

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

env_file:

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

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

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

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

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

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

tmpfs:

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

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

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

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

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

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

CPU

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

Память

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

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

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

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

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

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

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

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

Namespace и изоляция

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

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

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

Логирование

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

restart_policy:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Top-level volumes объявляет тома, которые можно переиспользовать между сервисами.

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

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

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

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

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

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

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

configs и secrets

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Заключение

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

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

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


Об авторе

ДК
Даниил Кулешов · Руководитель проекта Opensophy, DevSecOps-инженер (opensophy)

Автор этой статьи. Даниил Кулешов (opensophy) — руководитель проекта Opensophy и DevSecOps-инженер.