SSD Nodes Learn Hosting plans →
Посібники Matt ConnorВід Matt Connor

Docker Compose: YAML-якорі та extends без дублювання

Один блок налаштувань замість шести. Як працюють YAML-якорі, ключ злиття та extends у Docker Compose, чому якорі не бачать сусідній файл і як це перевірити.

Що таке YAML-якір у Docker Compose

YAML-якорі в Docker Compose дають описати спільний блок налаштувань один раз і підставити його в кожен сервіс. Якір оголошується знаком &, посилання на нього знаком *, а ключ злиття << вливає вміст якоря всередину мапи сервісу. Усю цю роботу робить YAML-парсер: він розгортає посилання ще до того, як Compose побачить структуру файлу.

Це має значення для дуже конкретного читача. У вас на VPS лежить один compose.yaml з п'ятьма чи шістьма сервісами, і в кожному сервісі повторюється той самий restart, той самий блок logging, ті самі TZ, PUID, PGID і схожий healthcheck. Зміна одного значення означає шість однакових правок, і на шостій ви щось пропустите. Якорі прибирають саме це повторення.

Далі йдуть три механізми, у порядку зростання можливостей: поле верхнього рівня x- з якорем, ключ злиття << усередині сервісу, і атрибут extends з ключем file: для повторного використання між різними файлами. Третій існує саме тому, що перші два не працюють за межами одного файлу.

Ставимо Docker Compose з репозиторію Docker

Пакет docker-compose зі стандартного репозиторію дистрибутива це стара реалізація на Python з командою через дефіс. Синтаксис extends і правила злиття за роки змінилися, тому ставте плагін з власного репозиторію Docker і працюйте командою docker compose без дефіса.

# Add Docker's official GPG key:
sudo apt update
sudo apt install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository to Apt sources:
sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF

sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
docker compose version

docker compose version має надрукувати номер версії плагіна. Запишіть його собі: правила злиття і тексти помилок залежать від версії, і без цього числа чужа порада з форуму вам не допоможе. Станом на вересень 2026 найсвіжіший реліз у репозиторії docker/compose має тег v5.5.1 і опублікований 3 вересня 2026. Пакет docker-compose-plugin може відставати від нього на кілька днів, тому орієнтуйтеся на те, що надрукувала саме ваша машина.

Якщо команда друкує щось на кілька мажорних версій старіше, ви запустили не той бінарник. Перевірте command -v docker-compose: стара версія часто лишається в системі поруч з новою.

Крок 1: поле x- верхнього рівня, яке тримає якір

Compose ігнорує будь-який ключ верхнього рівня, назва якого починається з x-. Це офіційне місце під розширення, і саме туди зручно покласти якір: Compose не спробує зробити з нього сервіс, а YAML-парсер усе одно прочитає блок.

x-service-defaults: &service-defaults
  restart: unless-stopped
  logging:
    driver: json-file
    options:
      max-size: "10m"
      max-file: "3"
  environment:
    TZ: Europe/Kyiv
    PUID: "1000"
    PGID: "1000"

&service-defaults це оголошення якоря. Ім'я довільне, але між & і іменем не має бути пробілу, бо тоді парсер прочитає рядок зовсім інакше. Блок має стояти у файлі вище за перше посилання на нього, бо YAML читає документ згори вниз і не знає наперед про якорі, яких ще не бачив.

Про PUID і PGID тут варто сказати окремо, бо саме ці два значення дублюють найчастіше: вони вирішують, від якого користувача контейнер пише у ваші каталоги на хості. Що саме вони роблять і чому файли інколи належать чужому UID, розібрано в поясненні PUID і PGID у контейнерах.

Крок 2: ключ злиття << усередині сервісу

services:
  sonarr:
    <<: *service-defaults
    image: lscr.io/linuxserver/sonarr:latest
    container_name: sonarr
    volumes:
      - ./sonarr/config:/config
    ports:
      - "8989:8989"

  radarr:
    <<: *service-defaults
    image: lscr.io/linuxserver/radarr:latest
    container_name: radarr
    volumes:
      - ./radarr/config:/config
    ports:
      - "7878:7878"

*service-defaults це псевдонім, посилання на раніше оголошений якір. Ключ << каже парсеру взяти мапу, на яку вказує псевдонім, і влити її ключі в цю мапу. Обидва сервіси тепер спираються на один опис restart, logging і environment, який лежить в одному місці файлу.

Такий файл найшвидше окупається на медійному стеку, де сервісів справді шість чи вісім і всі вони від одного постачальника образів. Повний набір з томами, портами і мережею показаний у складанні arr-стека з Sonarr і Radarr на VPS.

Як побачити результат: docker compose config

Це єдина команда, якій тут варто вірити. Вона читає задані файли, розгортає якорі, підставляє змінні, застосовує extends і друкує підсумкову модель у stdout. Контейнери при цьому не створюються і не змінюються.

docker compose config

Запускайте її після кожної правки якоря. Не здогадуйтеся, що вийде, прочитайте вивід. Якщо файл великий, docker compose config | less читається легше, а docker compose config --services просто перелічить імена сервісів, які Compose зрештою побачив. Обов'язково передавайте ті самі прапорці -f, з якими ви збираєтеся запускати стек, інакше ви дивитеся на іншу модель.

Що буде, якщо перевизначити ключ з якоря

Ось місце, де помиляються найчастіше.

Документація Compose формулює правило коротко: злиття через << стосується лише мап і не працює з послідовностями. Тому результат залежить від того, у якій формі написано ключ. environment можна записати мапою (TZ: Europe/Kyiv) і можна списком (- TZ=Europe/Kyiv). volumes і ports бувають тільки списками.

Зберіть маленьку лабораторію і подивіться самі. Створіть окремий каталог, щоб нічого не зачепити:

mkdir -p ~/anchors-lab
cd ~/anchors-lab

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

x-defaults: &defaults
  restart: unless-stopped
  environment:
    TZ: Europe/Kyiv
    LOG_LEVEL: info
  volumes:
    - ./shared:/shared

services:
  a:
    <<: *defaults
    image: alpine:3.21
    command: sleep 3600

  b:
    <<: *defaults
    image: alpine:3.21
    command: sleep 3600
    environment:
      LOG_LEVEL: debug

  c:
    <<: *defaults
    image: alpine:3.21
    command: sleep 3600
    volumes:
      - ./own:/own

Потім:

docker compose config

Знайдіть у виводі три сервіси і порівняйте їх між собою. Сервіс a показує якір без змін і слугує точкою відліку. Сервіс b показує, що сталося з мапою environment, коли перевизначили один її ключ: подивіться, чи лишився в ньому TZ. Сервіс c показує, що сталося зі списком volumes: подивіться, скільки там рядків. Цей вивід і є відповіддю для вашої версії Compose, і він надійніший за будь-який текст, включно з цим.

Зробіть це один раз власноруч, і далі ви будете знати, який з двох ключів можна спокійно перевизначати в сервісі, а який доведеться писати повністю.

Чому якір не бачить сусідній файл

YAML розв'язує якорі в межах одного документа. Псевдонім шукає якір у тому самому потоці, який зараз розбирає парсер. Коли ви запускаєте docker compose -f compose.yaml -f compose.prod.yaml up -d, Compose розбирає кожен файл окремо, а вже потім зливає готові моделі. На момент розбору compose.prod.yaml якоря з compose.yaml просто не існує.

Тому ось такий другий файл не збереться:

services:
  sonarr:
    <<: *service-defaults
    cpus: "1.5"

Перевірте це тією ж командою docker compose config з обома прапорцями -f і прочитайте, на що поскаржиться парсер: він назве псевдонім, якого не знайшов. Точне формулювання залежить від версії, тому дивіться свій вивід, а не чужий.

Це і є причина, чому в Compose окремо існує extends. Якорі належать формату YAML, а extends належить самому Compose, тому лише другий уміє відкрити інший файл.

extends з ключем file: для спільної бази

extends вказує на конкретний сервіс у конкретному файлі. Compose читає той сервіс і бере його як основу для вашого.

Файл common/common.yaml:

services:
  base:
    restart: unless-stopped
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
    environment:
      TZ: Europe/Kyiv

Файл media/compose.yaml у сусідньому каталозі:

services:
  sonarr:
    extends:
      file: ../common/common.yaml
      service: base
    image: lscr.io/linuxserver/sonarr:latest
    ports:
      - "8989:8989"

common.yaml сам по собі не є робочим стеком, бо в base немає образу. Це нормально: цей файл існує тільки як джерело для extends.

Правила, які варто знати наперед, бо вони прямо описані в специфікації Compose:

  • Відносні шляхи всередині сервіса, який ви розширюєте, перераховуються так, щоб і далі вказувати на той самий файл. Якщо common.yaml містить env_file: ./container.env, підсумкова модель покаже шлях, перерахований відносно вашого головного файлу.
  • Скалярні значення з вашого сервісу мають перевагу над значеннями з розширюваного.
  • У мапах ваші ключі перекривають однойменні ключі розширюваного сервісу, а решта ключів переноситься як є.
  • Елементи послідовностей об'єднуються в один список, і елементи з розширюваного сервісу йдуть першими.
  • Циклічні посилання не підтримуються, Compose поверне помилку, щойно їх виявить.

І найважливіше обмеження, про яке забувають: extends не тягне за собою те, на що розширюваний сервіс посилається. depends_on, links, volumes_from, іменовані томи, мережі, configs, secrets і форма service:{name} у ipc, pid чи network_mode мають бути оголошені у вашому файлі явно. Compose не імпортує ці ресурси автоматично, тому сервіс, який чудово працював у вихідному файлі, після extends може не знайти своєї мережі або тому.

Ще одна деталь: extends не підтримується при розгортанні через docker stack deploy. Якщо ви колись плануєте віддати цей стек у Swarm, тримайте це в голові.

І знову: docker compose config. Вона показує вже застосований extends, включно з перерахованими відносними шляхами, тому саме на її виводі видно, чи взялося те, що ви хотіли.

Помилки, через які це не працює

Пробіл після & або *. Записи & defaults і * defaults не є якорем і псевдонімом. Документація Compose окремо просить не ставити там пробіл.

Якір оголошено нижче за перше посилання на нього. Парсер іде згори вниз. Тримайте всі блоки x- на початку файлу, до розділу services:.

Спроба зібрати ім'я якоря зі змінної. Розв'язання якорів відбувається до підстановки змінних, тому конструкція виду &${NAME} не працює. Змінні заповнюють значення, але не імена якорів і псевдонімів.

Список там, де ви чекали злиття. Ключ << не зливає послідовності. Якщо потрібно, щоб сервіс мав спільний том плюс свій власний, винесіть окремий рядок тому в окремий якір (x-shared-volume: &shared-volume ./shared:/shared) і згадайте цей псевдонім елементом у списку volumes кожного сервісу.

Спільний healthcheck, скопійований усім підряд. Одна перевірка рідко підходить кільком різним застосункам, бо вона має питати саме про цей сервіс. Як написати робочу перевірку, що робить start_period і чому контейнер зависає в стані starting, розібрано в налаштуванні healthcheck у Docker Compose.

Межа: якорі не дають вам середовищ

Тут треба сказати прямо. Якорі та extends прибирають повторення всередині опису стека. Окремих середовищ вони не дають.

Якщо потрібен один стек у двох варіантах, наприклад з відкритими назовні портами на робочій машині і без них на VPS, це робота файлів перекриття: docker compose -f compose.yaml -f compose.prod.yaml up -d, де другий файл змінює тільки те, що відрізняється. Правила злиття між файлами інші за правила ключа <<, і вони описані в роботі з кількома compose-файлами і файлами перекриття.

Якщо ж відрізняється не структура, а самі значення (паролі, ключі API, домени), їхнє місце не в якорі. Винесіть їх у файл змінних поза git: env-файли та секрети в Docker Compose показує різницю між .env, який читає сам Compose, і env_file, який отримує контейнер.

Правило просте: якір прибирає однакове, а файл перекриття описує різне. Таємне не належить ні туди, ні туди.

І остання чесна заувага. Файл з якорями читається гірше, ніж файл без них, бо налаштування сервісу більше не видно повністю в одному місці. Для вас це вигідний обмін, поки ви пам'ятаєте про docker compose config. Для людини, яка відкриє цей файл через рік, ця команда буде першим, що їй треба показати.

FAQ

Чим <<: *defaults відрізняється від extends?

<< це ключ злиття самого YAML. Парсер розгортає його всередині одного документа, і він не бачить нічого поза межами цього файлу. extends це атрибут Compose: ви вказуєте file: і service:, і Compose відкриває інший файл та бере звідти сервіс як основу. Якщо спільні налаштування живуть у тому самому файлі, беріть <<, бо він коротший. Якщо ті самі налаштування ділять кілька окремих стеків, беріть extends.

Чи можна послатися на якір з іншого compose-файлу?

Ні. YAML розв'язує якорі в межах одного документа, а Compose розбирає кожен файл окремо і лише потім зливає готові моделі. Псевдонім у другому файлі не знайде якоря з першого, і розбір впаде з помилкою про невідомий псевдонім. Для спільного між файлами є extends з ключем file:, або звичайні файли перекриття через кілька прапорців -f.

Чому Compose не скаржиться на моє поле x-service-defaults?

Ключі верхнього рівня, назва яких починається з x-, зарезервовані під розширення, і Compose їх не інтерпретує. Саме тому туди безпечно класти якір: сервісу з нього не вийде, а YAML-парсер усе одно прочитає блок і дасть на нього послатися. Переконатися можна командою docker compose config --services, яка перелічить лише справжні сервіси.

Як подивитися підсумкову конфігурацію, нічого не запускаючи?

docker compose config читає всі задані файли, розгортає якорі, застосовує extends, підставляє змінні і друкує підсумкову модель у stdout. Контейнери вона не створює, не запускає і не зупиняє. Передавайте їй ті самі прапорці -f, з якими ви збираєтеся піднімати стек, інакше ви побачите не ту модель. Це і є перевірка, яку варто робити після кожної правки якоря.