Learning Platform
Глоссарий Troubleshooting
Урок 13.03 · 24 мин
Средний
dockercomposeoverridemerge

Override-файлы: base + dev + prod

В реальной жизни один compose.yml редко обслуживает все окружения. Локально хочется live edit кода и BIND mount; в CI — собранный образ и без портов наружу; на prod-сервере — restart policy, ресурсы, secrets. Compose решает это через override-файлы — несколько YAML-файлов, которые мерджатся в один эффективный compose. В этом уроке разбираем механику мерджа и канонический паттерн base+dev+prod.


Авто-мерж: compose.override.yml

Compose автоматически ищет два файла:

  1. compose.yml (base).
  2. compose.override.yml (override, опционально).

Если оба есть — мерджит их. compose.override.yml ПЕРЕОПРЕДЕЛЯЕТ или ДОПОЛНЯЕТ base.

Пример.

compose.yml:

services:
  app:
    image: myapp:1.0
    environment:
      LOG_LEVEL: info
    ports:
      - "8000:8000"

compose.override.yml:

services:
  app:
    environment:
      LOG_LEVEL: debug
      DEBUG: "1"
    volumes:
      - ./src:/app/src

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

services:
  app:
    image: myapp:1.0
    environment:
      LOG_LEVEL: debug      # переопределено
      DEBUG: "1"            # добавлено
    ports:
      - "8000:8000"         # из base
    volumes:
      - ./src:/app/src      # из override

Запускается через простую команду:

docker compose up -d
# Использует compose.yml + compose.override.yml автоматически.

Идиома: compose.yml в Git с base-конфигом, compose.override.yml в .gitignore или с dev-настройками. Каждый член команды может иметь свой override без правки common-файла.


Явный мерж через -f

docker compose -f compose.yml -f compose.prod.yml up -d

Compose мерджит файлы в порядке, в котором они указаны. Поздние перекрывают ранние. Сколько файлов — не ограничено.

compose.override.yml тогда уже не подцепляется автоматом, потому что использован явный список.

# Локальная разработка: автомерж.
docker compose up

# Прод: явные файлы.
docker compose -f compose.yml -f compose.prod.yml up

Паттерн base + dev + prod

Переменные среды в shell и как они попадают в процессы

Канонический setup:

project/
  compose.yml          # base: общие сервисы и connections
  compose.override.yml # local dev defaults (опционально, авто)
  compose.dev.yml      # explicit dev override (опционально, через -f)
  compose.prod.yml     # prod override (через -f)
  compose.test.yml     # CI test override (через -f)

compose.yml (base)

services:
  postgres:
    image: postgres:17
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?required}
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s

  app:
    image: myorg/etl-app:${APP_VERSION:-latest}
    depends_on:
      postgres:
        condition: service_healthy
    environment:
      DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/postgres

volumes:
  pgdata:

Минимум: что в общем для всех окружений.

compose.override.yml (auto, dev)

services:
  app:
    build: ./app                 # вместо image — собираем локально
    volumes:
      - ./app/src:/app/src       # live edit
    environment:
      DEBUG: "1"
      LOG_LEVEL: debug
    ports:
      - "127.0.0.1:8000:8000"

  postgres:
    ports:
      - "127.0.0.1:5432:5432"    # на dev открыт для psql

При docker compose up без флагов — этот override применяется.

compose.prod.yml

services:
  app:
    restart: unless-stopped
    deploy:
      resources:
        limits: { cpus: '1.0', memory: 512M }
        reservations: { memory: 256M }
    logging:
      driver: json-file
      options:
        max-size: 10m
        max-file: "3"

  postgres:
    restart: unless-stopped
    deploy:
      resources:
        limits: { cpus: '2.0', memory: 2G }
    logging:
      driver: json-file
      options:
        max-size: 50m
        max-file: "5"

Запуск:

docker compose -f compose.yml -f compose.prod.yml up -d

Чтобы compose не подхватил compose.override.yml — явный -f отменяет авто-мерж.

compose.test.yml

services:
  app:
    environment:
      DATABASE_URL: postgresql://postgres:test@postgres:5432/test
    command: ["pytest", "-x", "tests/"]
    profiles: ["test"]

  postgres:
    environment:
      POSTGRES_PASSWORD: test
      POSTGRES_DB: test

Запуск в CI:

docker compose -f compose.yml -f compose.test.yml --profile test up --abort-on-container-exit

Правила мерджа

Это самая интересная часть, потому что от неё зависят неочевидные эффекты.

Тип значенияПоведение
Скаляр (string, int, bool)Override полностью заменяет base
Map (environment, labels, build)Deep merge — ключи мерджатся, override-ключ перебивает
Sequence (volumes, ports, dns)Append — base + override (с дедупликацией)
Image vs BuildОсобый случай (см. ниже)

Map (deep merge)

# base
environment:
  A: 1
  B: 2

# override
environment:
  B: 22
  C: 3

# merged
environment:
  A: 1
  B: 22  # override победил
  C: 3   # добавлен

То же для labels, build.args.

Sequence (append)

# base
volumes:
  - data:/data

# override
volumes:
  - logs:/logs

# merged
volumes:
  - data:/data
  - logs:/logs

Обе записи в результате. Это часто неожиданно: если ты в override хочешь УБРАТЬ volume из base — это не сработает. Можно только добавить.

Для портов и DNS — то же поведение.

WARNING

Если в base есть ports: ["5432:5432"], и ты хочешь в prod-override УБРАТЬ публикацию — это нельзя сделать через merge. Compose добавит. Решение: не публиковать порт в base, добавлять только в dev-override. Или развести base так, чтобы prod вообще не зависел от base в этой части.

Image vs Build

# base
services:
  app:
    image: myorg/app:v1.0

# override
services:
  app:
    build: ./app

В этом случае compose не мерджит, а выбирает: если в override есть build — используется build и игнорируется image из base (или используется image как тэг для собранного). Тоже наоборот.

Это сделано осознанно: смешать «бери из registry» и «собирай локально» нельзя — это разные операции.


docker compose config — что эффективно

Главный инструмент дебага:

docker compose -f compose.yml -f compose.prod.yml config

Выведет итоговый эффективный compose после всех мерджей и интерполяции. Если что-то не подставилось — это видно.

docker compose -f compose.yml -f compose.prod.yml config --services
# Список сервисов

docker compose -f compose.yml -f compose.prod.yml config --hash app
# Hash, который compose использует для проверки изменений app

docker compose -f compose.yml -f compose.prod.yml config --no-interpolate
# БЕЗ интерполяции — видишь сырой YAML

COMPOSE_FILE env var

Альтернатива -f:

export COMPOSE_FILE=compose.yml:compose.prod.yml
docker compose up -d
# То же, что -f compose.yml -f compose.prod.yml

В docker-compose.env или .env:

COMPOSE_FILE=compose.yml:compose.prod.yml
COMPOSE_PROJECT_NAME=etl-prod

Это даёт «по умолчанию использовать prod-комбинацию» без необходимости помнить флаги.

Эффективный compose после мерджа
compose.yml (base)Общие сервисы, connections, structure. Минимально-возможный common-конфиг
merge
compose.override.yml (auto)Auto-merged для local dev. В .gitignore или с dev-параметрами
compose.prod.yml (explicit -f)Активируется только через -f. Содержит restart, resources, logging для production
merge с base
Effective composeИтог: base + (auto override ИЛИ explicit override). Это то, что compose реально использует для запуска

Попробуй сам

mkdir -p override-demo && cd override-demo

# Base.
cat > compose.yml <<'YAML'
services:
  app:
    image: alpine:3.20
    environment:
      MODE: base
      A: from-base
    command: sh -c 'env | grep -E "MODE|A=|B="; sleep 100'
YAML

# Auto-override (dev defaults).
cat > compose.override.yml <<'YAML'
services:
  app:
    environment:
      MODE: dev    # перебивает
      B: from-override
YAML

# 1. Auto-merge.
docker compose up
# Видишь: MODE=dev, A=from-base, B=from-override.

# 2. Что эффективно?
docker compose down
docker compose config

# Prod override.
cat > compose.prod.yml <<'YAML'
services:
  app:
    environment:
      MODE: prod
      DEBUG: "0"
    command: sh -c 'env | grep -E "MODE|DEBUG"; sleep 100'
YAML

# 3. Явный -f.
docker compose -f compose.yml -f compose.prod.yml up
# Видишь: MODE=prod, DEBUG=0. Override-файл проигнорирован.

# 4. Эффективный prod-config.
docker compose -f compose.yml -f compose.prod.yml config

# Cleanup.
docker compose -f compose.yml -f compose.prod.yml down
cd .. && rm -rf override-demo
TIP

Минимальный практический setup для DE-проекта: compose.yml в Git (base), compose.override.yml в .gitignore для каждого dev-машины свой. На prod-сервере — compose.yml + compose.prod.yml явно через -f. Это даёт чистоту: общее в Git, личное — нет, prod-настройки версионируются отдельно.

В следующем уроке — secrets и configs: безопасный способ передачи паролей в контейнер, чтобы они не светились в docker inspect.


Проверка знанийKnowledge check
У тебя в compose.yml : app: ports: ["8000:8000"] . В compose.prod.yml хочешь убрать публикацию порта (на проде nginx-proxy перед app). Можно ли это сделать через merge override-файла? Объясни механику merge для sequence и предложи правильное решение.
ОтветAnswer
Нет, нельзя убрать через merge. Compose-правило merge для sequence (включая ports, volumes, dns, expose, labels-как-list): override-список ДОБАВЛЯЕТСЯ к base-списку, дедупликация по полному значению строки. Это значит, что если в base ports: ["8000:8000"], а в override ports: [] (пустой список) — результат всё равно ["8000:8000"]. Если override ports: ["9000:9000"] — итог ["8000:8000", "9000:9000"]. Решение: не публиковать порт в base, добавлять только в dev/local. base: app: image: ... (без ports). compose.override.yml для dev: app: ports: ["127.0.0.1:8000:8000"]. compose.prod.yml: app: (без ports — порт остаётся внутри сети, к нему ходит только nginx-proxy на той же compose-сети). Альтернатива — использовать profiles для публикации: app: ports: ... profiles: [local], но это переусложнение. Правило: всё, что хочешь иметь возможность УБРАТЬ в override, не клади в base.

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 4. Как compose обращается с файлами compose.yml и compose.override.yml?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5