Docker Compose: использование нескольких файлов
Узнайте, как Docker Compose объединяет файлы конфигурации, почему порт остается занятым при переопределении и как правильно использовать include для dev и prod сред.
Как 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 считывает только указанный файл и игнорирует переопределение; именно на этом свойстве строится шаблон для сред разработки и эксплуатации, описанный далее в этом руководстве.
На сервере это работает в обе стороны. Файл переопределений, оставленный в каталоге развёртывания, загружается каждой командой bare docker compose, выполненной из этого каталога, включая команду, которую запускает cron job. Поэтому production stack может начать монтировать исходный каталог через bind mount, хотя никто не собирался включать его в поставку. Выполните docker compose config после каждого развёртывания и проверьте полученный вывод. Если развёртывание выполняется без участия оператора, проверка полезна только в том случае, если вы получите уведомление об ошибке. Для этого нужен канал отправки уведомлений, например self-hosted ntfy server, в который cron job или systemd OnFailure unit может отправить сообщение.
Порядок использования -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 не находит. Риск выше для стека, обновления которого выполняются разовыми командами. Например, это может быть шаг миграции базы данных в самостоятельно размещённой системе поддержки Chatwoot. Если docker compose run запустить с неправильным списком файлов, команда незаметно обратится к другой модели, отличной от той, которую уже используют ваши сервисы. Вместо этого задайте список один раз с помощью переменной окружения 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 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, так как переопределение полностью перезаписывает строку. - Словари (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, так как перед ним будет стоять 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 до ваших правил firewall. Механизм описан в почему опубликованные порты Docker обходят ufw.
Существует два способа исправления. Явный способ — использование тега !override, который заменяет весь атрибут целиком и пропускает правила слияния:
services:
web:
ports: !override
- "127.0.0.1:8080:80"!override требует Docker Compose версии 2.24.4 или новее. Портативный способ не требует никаких тегов: исключите ports из базового файла полностью и объявляйте его только в файлах, специфичных для окружения. Если нечего объединять, то нечему и «утекать». Этот шаблон используется в приведенном ниже рабочем примере.
Удаление значения из базового набора файлов
!reset удаляет атрибут, возвращая его к значению по умолчанию или к null. Команда принимает значение, но игнорирует его, поэтому укажите любой корректный пустой параметр.
services:
web:
ports: !reset []
environment:
DEBUG: !reset null!reset требует Compose версии 2.24 или новее. Используйте эту возможность, если базовый файл нельзя редактировать, например, если это сторонний фрагмент. Опубликованный upstream-стек — именно такой случай: файл Compose для самостоятельно развернутого рабочего пространства AFFiNE описывает четыре контейнера, которые вы не создавали, а !reset позволяет очистить один атрибут у одного из них без создания форка файла и необходимости отслеживать его изменения.
include, для стеков, собранных из частей
include подгружает другое приложение Compose в вашу модель. Это элемент верхнего уровня, а не флаг.
include:
- path: ../commons/compose.yamlКаждый путь в include загружается как отдельная модель приложения Compose со своим собственным рабочим каталогом. Поэтому относительные пути внутри этого файла разрешаются относительно его собственного расположения. В этом заключается главное отличие от -f и причина, по которой include является подходящим инструментом, когда фрагмент находится в другой папке или другом репозитории. Это типичная структура стороннего стека, который вы не создавали: многосервисный файл Compose для самостоятельно развернутого Authentik SSO может находиться в собственном каталоге с корректными относительными путями, в то время как ваш файл остается сфокусированным на ваших собственных сервисах.
Развернутая форма записи поддерживает дополнительные параметры.
include:
- path:
- ../monitoring/compose.yaml
- ../monitoring/compose.vps.yaml
project_directory: ../monitoring
env_file: ../monitoring/.envpath принимает список файлов, которые объединяются по стандартным правилам перед тем, как результат будет добавлен в вашу модель. project_directory задает базовый путь, используемый для разрешения относительных путей во включенном файле. env_file предоставляет включенному файлу собственные переменные для интерполяции, что предотвращает неявное считывание общим фрагментом файла .env вашего проекта. include требует Compose версии v2.20.0 или новее. Эти же параметры подходят для добавления одноконтейнерного дополнения к уже работающему стеку, например Halcyon, который меняет интерфейс библиотеки Jellyfin на стиль видеопроката 90-х: его файл сохраняет собственный тег образа и свой env_file, поэтому обновление дополнения никогда не потребует внесения правок в файл вашего медиа-стека.
Дублирование имен ресурсов в вашем файле и во включенном файле вызывает ошибку, а не тихое слияние, и это сделано намеренно. Чтобы изменить что-то, объявленное во включенном файле, внесите изменения в compose.override.yaml: переопределение применяется к уже собранной модели, поэтому оно может изменять включенные ресурсы, не вступая с ними в конфликт. Эта практика наиболее полезна для стеков, чей исходный файл перезаписывается при каждом релизе, например, для многоконтейнерных фотосерверов, рассмотренных в сравнении PhotoPrism и Immich, где привязка к localhost или дополнительный том должны находиться в вашем переопределении, а не в файле, который будет заменен при следующем обновлении.
Кратко: 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 заставляет приложение дожидаться готовности базы данных, а не просто факта запуска контейнера, подробнее см. в 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 выполняет слияние по ключам. Bind mount и два открытых порта — это дополнительные параметры, а порт базы данных привязан к 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 не был прочитан. Поэтому команда для разработки, bind mount исходного кода и публичный порт 3000 не влияют на production, даже если файл находится в той же директории. Порт 8000 доступен только через localhost и готов к работе с прокси: см. запуск нескольких приложений за Traefik при добавлении второго сервиса.
Установите COMPOSE_FILE=compose.yaml:compose.prod.yaml в файле .env на сервере, и остальные команды снова станут обычными вызовами docker compose logs -f app.
Стек из одного сервиса строится по такому же принципу, так как трекер тренировок openGym должен отвечать по TLS через прокси до регистрации первого passkey, а базовый файл без секции ports предотвращает случайное открытие портов, которые могут конфликтовать с прокси.
Изучите объединенную модель перед развертыванием
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 mount пуст, и 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 или новее).