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 автоматически ищет два файла:
compose.yml(base).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 — то же поведение.
Если в 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-комбинацию» без необходимости помнить флаги.
Попробуй сам
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
Минимальный практический setup для DE-проекта: compose.yml в Git (base), compose.override.yml в .gitignore для каждого dev-машины свой. На prod-сервере — compose.yml + compose.prod.yml явно через -f. Это даёт чистоту: общее в Git, личное — нет, prod-настройки версионируются отдельно.
В следующем уроке — secrets и configs: безопасный способ передачи паролей в контейнер, чтобы они не светились в docker inspect.