Docker Compose: кілька файлів і правильне злиття
Дізнайтеся, як compose.override.yaml завантажується сам, як порядок файлів змінює модель, чому ports залишає порт відкритим і як розділити dev та prod.
Що Compose робить із кількома файлами
Docker Compose може створити один проєкт із кількох файлів. Він читає їх у порядку отримання та об’єднує в одну модель, тому пізніший файл має пріоритет для будь-якого конфліктного значення. Це можна зробити з командного рядка двома способами: за допомогою файла перевизначень, який Compose завантажує автоматично, або за допомогою прапора -f, який ви вказуєте вручну. Третій спосіб реалізовано безпосередньо у файлі через елемент include. Він працює інакше, ніж обидва попередні.
Об’єднання не є простим перезаписуванням. Відображення об’єднуються за ключами, послідовності доповнюються, а невеликий набір полів замінюється повністю. Саме ця відмінність спричиняє несподівані результати, а список ports найчастіше стає їхньою причиною.
Усе нижче стосується Compose v2 і плагіна docker compose, а не старого скрипту docker-compose. Виконайте docker compose version, щоб перевірити версію. Якщо ви ще не створили файл Compose, спочатку перегляньте посібник з основ Docker Compose, а потім поверніться сюди.
Файл перевизначень, який Compose завантажує автоматично
Запустіть docker compose up без прапорця -f. Compose шукає compose.yaml або docker-compose.yaml у робочому каталозі, а потім у батьківських каталогах. Якщо файл перевизначень розташований поруч із базовим файлом, Compose автоматично завантажує його другим.
ls compose.yaml compose.override.yaml
docker compose up -dЯкщо присутні обидва файли, результат такий самий, якби ви вказали їх вручну.
docker compose -f compose.yaml -f compose.override.yaml up -dCompose розпізнає імена compose.override.yaml, compose.override.yml, а також старіші docker-compose.override.yml і docker-compose.override.yaml. Файл з будь-яким іншим іменем, наприклад compose.dev.yaml, завантажується лише тоді, коли ви вказуєте його за допомогою -f.
Щойно ви передаєте один -f, автоматичне завантаження припиняється. docker compose -f compose.yaml up читає лише цей файл і ігнорує файл перевизначень. Саме на цій властивості ґрунтується описаний далі шаблон для dev і prod.
На сервері це може мати небажані наслідки. Файл перевизначень у каталозі розгортання завантажується кожною командою docker compose без явних параметрів, запущеною з цього каталогу, зокрема командою, яку запускає cron. Через це production-стек може змонтувати через bind mount каталог із вихідним кодом, який ніхто не планував розгортати. Після кожного розгортання запускайте docker compose config і перевіряйте результат.
Порядок із -f і визначення відносних шляхів
Compose створює конфігурацію в порядку, у якому ви передаєте файли. Наступні файли перевизначають параметри попередніх і додають нові. Зліва направо діє правило: останнє значення має пріоритет.
docker compose -f compose.yaml -f compose.prod.yaml config
docker compose -f compose.yaml -f compose.prod.yaml up -dДля кожної команди в цьому проєкті потрібен той самий список файлів. Якщо виконати up із двома файлами, а logs — з одним, ви працюватимете з іншою об’єднаною моделлю. Через це Compose може повідомити, що сервіс не існує. Замість цього задайте список один раз за допомогою змінної середовища COMPOSE_FILE.
export COMPOSE_FILE=compose.yaml:compose.prod.yaml
docker compose config
docker compose up -dУ Linux роздільником є :, а COMPOSE_PATH_SEPARATOR змінює його. COMPOSE_FILE також можна вказати у файлі проєкту .env. Тоді цей параметр зберігатиметься разом із checkout, а не лише в історії shell. Значення, явно задане в командному рядку, має пріоритет над змінною середовища.
Тепер правило, через яке виникають проблеми з bind mounts. Якщо використати кілька файлів із -f, усі відносні шляхи в усіх цих файлах визначаються відносно каталогу першого файлу, а не файлу, у якому вони вказані. Запишіть ./data:/var/lib/postgresql/data у deploy/prod/compose.prod.yaml, і Compose все одно шукатиме ./data поруч із базовим файлом. Потім Docker створить порожній каталог за неправильним шляхом, і контейнер запуститься без даних. Це виглядає як втрата даних, але нею не є. Передайте --project-directory, щоб самостійно задати базовий шлях, або використайте include. Він визначає шлях до кожного файлу відносно його власного каталогу.
Назва проєкту визначається з того самого базового каталогу. Тому зміна першого файлу може перейменувати проєкт. Після перейменування проєкту створюються нові назви контейнерів і томів, а старий том залишається на диску під старою назвою. Зафіксуйте назву за допомогою параметра верхнього рівня name: у базовому файлі.
name: myappЯкі поля об’єднуються, а які замінюються
Compose об’єднує поля за типом значення, а не за назвою поля.
- Поля з одним значенням замінюються.
image,command,entrypointіmem_limitбезпосередньо приймають пізніше значення. Не можна додати один аргумент доcommand, оскільки перевизначення перезаписує весь рядок. - Відображення об’єднуються за ключами.
environment,labels,volumesіdevicesзберігають усі ключі з обох файлів, а пізніший файл має пріоритет для ключів, наявних в обох файлах. Дляenvironmentіlabelsключем є назва змінної або мітки. Дляvolumesіdevicesключем є шлях контейнера. - Послідовності додаються.
dns,dns_search,expose,tmpfsіexternal_linksоб’єднуються шляхом конкатенації. Якщо базова конфігурація міститьexpose: ["3000"], а конфігурація перевизначення —["4000", "5000"], результатом буде["3000", "4000", "5000"].
Чотири послідовності мають ключ ідентифікації, тому записи з однаковим ключем об’єднуються, а не додаються. volumes, secrets і configs порівнюються за target. ports порівнюється за комбінацією ip, target, published і protocol.
Уважно перечитайте правило ports, оскільки саме тут найчастіше виникає помилка. Два записи портів є одним записом лише тоді, коли всі чотири частини збігаються. Якщо змінити будь-яку з них, Compose вважає запис другим, незалежним портом і зберігає обидва.
Чому ваш порт досі опублікований після перевизначення
Базовий файл, який публікує сервіс на кожному інтерфейсі:
services:
web:
image: nginx:1.27
ports:
- "8080:80"Перевизначення, яке прив’язує його лише до localhost, оскільки перед ним працюватиме reverse proxy:
services:
web:
ports:
- "127.0.0.1:8080:80"Перевірте результат, перш ніж вважати, що зміни застосовано.
docker compose -f compose.yaml -f compose.prod.yaml configОбидва записи є у виведенні. Частина ip відрізняється: 0.0.0.0 проти 127.0.0.1. Тому під час об’єднання конфігурації це два різні порти, а загальнодоступна прив’язка, яку ви намагалися видалити, досі присутня в моделі. У Docker це важливіше, ніж в інших середовищах, оскільки опублікований порт додається до iptables перед правилами вашого брандмауера. Цей механізм описано в розділі чому опубліковані порти Docker обходять ufw.
Є два способи виправлення. Явний спосіб — тег !override. Він замінює весь атрибут і не застосовує правила об’єднання:
services:
web:
ports: !override
- "127.0.0.1:8080:80"Для !override потрібен Compose v2.24.4 або новіша версія. Переносимий спосіб не потребує тегів: повністю вилучіть ports з базового файла та оголошуйте його лише у файлах для конкретних середовищ. Якщо об’єднувати нічого, витікати також нічому. Саме цей підхід використано в наведеному нижче прикладі.
Видалення значення, заданого в базовому файлі
!reset видаляє атрибут, повертаючи для нього значення за замовчуванням або null. Команда приймає значення та ігнорує його, тому вкажіть будь-яке допустиме порожнє значення.
services:
web:
ports: !reset []
environment:
DEBUG: !reset null!reset потребує Compose версії 2.24 або новішої. Використовуйте її, якщо базовий файл не можна редагувати, наприклад, якщо це фрагмент постачальника, який ви підключаєте.
include для стеків, зібраних із компонентів
include додає інший застосунок Compose до вашої моделі. Це елемент верхнього рівня, а не прапорець.
include:
- path: ../commons/compose.yamlКожен шлях у include завантажується як окрема модель застосунку Compose із власним каталогом проєкту. Тому відносні шляхи всередині цього файлу визначаються відносно каталогу самого файлу. У цьому полягає фактична відмінність від -f. Саме тому include слід використовувати, коли фрагмент розташований в іншій папці або іншому репозиторії.
Розширений синтаксис підтримує додаткові параметри.
include:
- path:
- ../monitoring/compose.yaml
- ../monitoring/compose.vps.yaml
project_directory: ../monitoring
env_file: ../monitoring/.envpath приймає список, а ці файли об’єднуються за стандартними правилами до додавання результату до вашої моделі. project_directory задає базовий шлях для визначення відносних шляхів у включеному файлі. env_file надає включеному файлу власні змінні для інтерполяції. Це запобігає непомітному читанню спільним фрагментом .env вашого проєкту. Для include потрібна Compose версії 2.20.0 або новішої.
Дублікати назв ресурсів у вашому файлі та включеному файлі вважаються помилкою, а не непомітно об’єднуються. Це зроблено навмисно. Щоб змінити оголошення у включеному файлі, внесіть зміну до compose.override.yaml. Перевизначення застосовується до зібраної моделі, тому може змінювати включені ресурси без конфлікту з ними.
Коротко: include об’єднує окремі застосунки, а -f додає конфігурацію до одного застосунку.
Розділення dev і prod на одному VPS
Ось увесь шаблон у трьох файлах. Базовий файл оголошує спільні для всіх параметри й узагалі не публікує портів.
name: myapp
services:
app:
image: ghcr.io/example/app:1.4.2
environment:
DATABASE_URL: postgres://app:${POSTGRES_PASSWORD}@db:5432/app
LOG_LEVEL: info
depends_on:
db:
condition: service_healthy
restart: unless-stopped
db:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_DB: app
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
volumes:
db_data:Умова depends_on змушує застосунок чекати на базу даних, яка відповідає, а не на контейнер, який лише існує. Це пояснюється в розділі перевірки стану та умови depends_on. Значення POSTGRES_PASSWORD підставляється з файлу проекту .env, який ніколи не має потрапляти до git. Безпечніші варіанти описано в розділі файли env і секрети Compose.
Далі — compose.override.yaml, який Compose завантажує автоматично. Це файл розробника.
services:
app:
build: .
command: npm run dev
environment:
LOG_LEVEL: debug
ports:
- "3000:3000"
volumes:
- ./src:/app/src
db:
ports:
- "127.0.0.1:5432:5432"На ноутбуці звичайна команда docker compose up об’єднує ці два файли. command замінює значення за замовчуванням для образу, оскільки це одиночне значення. LOG_LEVEL замінює info, оскільки environment об’єднує за ключем. Прив’язане сховище та два опубліковані порти є лише додаваннями, а порт бази даних прив’язано до localhost, щоб ноутбук у спільній мережі не відкривав PostgreSQL для інших пристроїв.
Нарешті, compose.prod.yaml. Compose не шукає файл із такою назвою, тому він ніколи не завантажується випадково.
services:
app:
ports:
- "127.0.0.1:8000:3000"
deploy:
resources:
limits:
memory: 512MНа VPS ви вказуєте обидва файли, і саме це виключає override-файл.
docker compose -f compose.yaml -f compose.prod.yaml config
docker compose -f compose.yaml -f compose.prod.yaml up -d
docker compose -f compose.yaml -f compose.prod.yaml psКоманда ps має показати обидва сервіси як запущені, а db — значення (healthy). Оскільки ви передали -f, файл compose.override.yaml не читався. Тому команда dev, прив’язане сховище з вихідним кодом і публічний порт 3000 не можуть потрапити у production, навіть якщо файл лежить у тому самому каталозі. Порт 8000 доступний лише через localhost і готовий для проксі. Коли додасте другий сервіс, дивіться запуск кількох застосунків за Traefik.
Установіть COMPOSE_FILE=compose.yaml:compose.prod.yaml у .env сервера, і решта команд знову зводитиметься до звичайного docker compose logs -f app.
Перегляньте об’єднану модель перед розгортанням
docker compose config виводить повністю об’єднану модель із підставленими значеннями. Це не попередній перегляд. Це точні вхідні дані, з якими працюватиме Compose. Тому якщо результат не відповідає вашим очікуванням, правильним є саме результат.
docker compose -f compose.yaml -f compose.prod.yaml config
docker compose -f compose.yaml -f compose.prod.yaml config --no-interpolate
docker compose -f compose.yaml -f compose.prod.yaml config --services--no-interpolate не розгортає ${VAR}. Використовуйте його перед вставленням результату будь-куди, оскільки звичайний config виводить усі розкриті секрети у відкритому тексті. --services виводить лише назви сервісів. Це швидкий спосіб перевірити, що include підключив саме те, що ви очікували.
Режими збоїв і те, що ви побачите
no configuration file provided: not found. Compose не знайшов, що читати. Ви перебуваєте поза каталогом проєкту, або COMPOSE_FILE вказує на шлях, якого не існує. Compose шукає батьківські каталоги для стандартного базового файлу, але не шукає файл, який ви вказали самостійно.
WARN[0000] The "POSTGRES_PASSWORD" variable is not set. Defaulting to a blank string. Інтерполяція використовує файл проєкту .env і середовище оболонки, а каталогом проєкту є каталог першого файлу -f. Розгортання з каталогу, відмінного від каталогу, де міститься .env, спричиняє це попередження, після чого база даних відхиляє кожне підключення.
Ваша зміна override-файлу не відображається в docker compose config. Або ви передали -f, що вимикає автоматичне завантаження override-файлів, або Compose знайшов compose.yaml у батьківському каталозі, а ваш override-файл розташований не поруч із ним. Запуск docker compose config без інших аргументів покаже, яку модель насправді створює Compose.
Bind mount порожній, і Docker створив каталог, якого ви не запитували. Відносний шлях було визначено відносно каталогу першого файлу. Виправте шлях, передайте --project-directory або перемістіть фрагмент за include.
Контейнери запускаються з новими іменами, а том виглядає порожнім. Назва проєкту змінилася, оскільки вона залежить від каталогу першого файлу. Додайте верхньорівневий name: до базового файлу, і назви більше не змінюватимуться. Старий том усе ще існує зі старим префіксом; його покаже docker volume ls.
Порт, який ви видалили в override-файлі, усе ще відкритий. Об’єднання ports додало запис замість його заміни. Перевірте результат за допомогою docker compose config, а потім використайте !override або перемістіть ports за межі базового файлу.
FAQ
Чи завантажує Compose файл compose.override.yaml автоматично?
Так, коли ви запускаєте docker compose без прапорця -f. Compose шукає в робочому каталозі та його батьківських каталогах compose.yaml або docker-compose.yaml. Якщо файл перевизначень розташований поруч із ним, цей файл завантажується другим. Розпізнані назви: compose.override.yaml, compose.override.yml, docker-compose.override.yml і docker-compose.override.yaml. Передавання будь-якого -f вимикає цю поведінку, тому docker compose -f compose.yaml up читає лише один файл.
У якому порядку об’єднуються кілька файлів -f?
Зліва направо. Compose створює конфігурацію в порядку, у якому ви передаєте файли. Кожен файл перевизначає та доповнює попередні, тому під час конфлікту перевагу має останній файл у командному рядку. Той самий список потрібно використовувати для кожної команди в цьому проєкті. Саме для цього призначено COMPOSE_FILE=compose.yaml:compose.prod.yaml.
Чому порт усе ще опублікований після його перевизначення?
Тому що записи ports ідентифікуються всією сукупністю ip, target, published і protocol. Перевизначення 127.0.0.1:8080:80 для базового значення 8080:80 відрізняється частиною ip. Тому Compose сприймає його як другий порт і зберігає обидва. Запустіть docker compose config, і ви побачите обидва записи. Використовуйте ports: !override у Compose v2.24.4 або новішої версії. Або не додавайте ports до базового файлу, щоб не було запису для об’єднання.
У чому різниця між include і -f?
-f об’єднує кілька файлів в одну програму. Відносні шляхи в кожному файлі визначаються відносно каталогу першого файлу. include підключає окрему програму Compose. Кожен підключений файл зберігає власний каталог проєкту, тому його відносні шляхи визначаються відносно цього каталогу. Використовуйте -f для шарів середовища власного стека, а include — для фрагмента, який підтримується в іншому місці. Для include потрібен Compose v2.20.0 або новішої версії.
Як видалити значення, задане в базовому файлі?
Використовуйте тег !reset у Compose v2.24 або новішої версії. Запишіть ports: !reset [] або MY_VAR: !reset null у файлі перевизначень, і атрибут повернеться до значення за замовчуванням або до null. Значення, передане тегу, є обов’язковим, але ігнорується. Якщо потрібно замінити атрибут, а не очистити його, використовуйте !override. Для цього потрібен v2.24.4 або новіша версія.