Docker Compose: работа с несколькими файлами конфигурации
Узнайте, как Docker Compose объединяет файлы, почему override.yaml загружается автоматически и как избежать конфликтов портов при использовании директивы include в проектах.
Как Compose работает с несколькими файлами
Docker Compose может собрать один проект из нескольких файлов. Он считывает их в порядке получения и объединяет в единую модель, поэтому при возникновении конфликтов значений приоритет отдается файлу, который был обработан позже. Для этого из командной строки используются два механизма: файл переопределения, который Compose загружает автоматически, и флаг -f, который вы передаете вручную. Третий механизм находится внутри самого файла — это элемент include, который работает иначе, чем первые два.
Объединение не является простой перезаписью. Отображения (mappings) объединяются по ключам, последовательности (sequences) дополняются, а небольшой набор полей заменяется целиком. Именно из-за этой разницы возникают неожиданные результаты, а список 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 считывает только указанный файл и игнорирует переопределение; на этом свойстве строится шаблон для сред разработки и эксплуатации, описанный далее в этом руководстве.
На сервере это работает в обе стороны. Файл переопределения, оставленный в каталоге развертывания, будет загружаться при каждой команде docker compose, запущенной из этого каталога, включая команды, выполняемые по расписанию cron. Именно так в производственной среде может оказаться примонтированным каталог с исходным кодом, который не предназначался для развертывания. Выполняйте 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 проекта, что сделает ее частью репозитория, а не истории вашей оболочки. Любое значение, явно заданное в командной строке, имеет приоритет над переменной окружения.
Теперь правило, которое нарушает работу bind-монтирования. При использовании нескольких файлов с -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, так как переопределение полностью перезаписывает строку. - Отображения (mappings) объединяются по ключам.
environment,labels,volumesиdevicesсохраняют все ключи из обоих файлов, при этом значение из файла, считанного позже, имеет приоритет, если ключ присутствует в обоих. Дляenvironmentиlabelsключом является имя переменной или метки. Дляvolumesиdevicesключом является путь к контейнеру. - Последовательности (sequences) объединяются путем добавления.
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, так как перед ним будет стоять обратный прокси-сервер:
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 версии 2.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 накладывает конфигурацию на одно приложение.
Разделение среды разработки и продакшена на одном 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 заставляет приложение ожидать готовности базы данных, а не просто факта запуска контейнера, как описано в healthchecks и условия 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. Его имя не входит в список стандартных, поэтому он не загружается случайно.
services:
app:
ports:
- "127.0.0.1:8000:3000"
deploy:
resources:
limits:
memory: 512MНа VPS необходимо явно указать оба файла; именно это исключает использование файла переопределения.
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 не был прочитан. Таким образом, команда для разработки, примонтированный исходный код и публичный порт 3000 не влияют на продакшен, даже если файл находится в той же директории. Порт 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, приводит к появлению этого предупреждения и последующему отказу базы данных в приеме любых соединений.
Ваши изменения в файле переопределения не отображаются в docker compose config. Либо вы передали флаг -f, который отключает автоматическую загрузку переопределений, либо Compose нашел compose.yaml в родительском каталоге, а ваш файл переопределения находится не рядом с ним. Запуск docker compose config без дополнительных аргументов покажет, какая модель фактически создается Compose.
Bind-монтирование пусто, и Docker создал каталог, который вы не запрашивали. Относительный путь был разрешен относительно каталога первого файла. Исправьте путь, передайте --project-directory или переместите фрагмент за include.
Контейнеры возвращаются с новыми именами, а том выглядит пустым. Имя проекта изменилось, так как оно зависит от каталога первого файла. Добавьте параметр name: верхнего уровня в базовый файл, чтобы имена перестали меняться. Старый том по-прежнему существует под старым префиксом, и docker volume ls покажет его.
Порт, который вы удалили в переопределении, все еще открыт. Слияние 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 версии 2.24.4 или новее, либо исключите ports из базового файла, чтобы не возникало конфликтов при объединении.
В чем разница между include и -f?
-f накладывает несколько файлов на одно приложение, при этом каждый относительный путь в каждом файле разрешается относительно каталога первого файла. include подгружает отдельное приложение Compose, и каждый включенный путь сохраняет свой собственный каталог проекта, поэтому относительные пути разрешаются относительно него самого. Используйте -f для уровней окружения вашего собственного стека, а include — для фрагмента, поддерживаемого отдельно. include требует Compose версии 2.20.0 или новее.
Как удалить значение, заданное в базовом файле?
Используйте тег !reset в Compose версии 2.24 или новее. Укажите ports: !reset [] или MY_VAR: !reset null в переопределяющем файле, и атрибут вернется к значению по умолчанию или к null. Значение, присвоенное тегу, является обязательным, но игнорируется. Если вы хотите заменить атрибут, а не очистить его, используйте !override; эта функция требует версию 2.24.4 или новее.