7 приемов Docker Compose, которые наведут порядок в вашем стеке

Якоря YAML, healthcheck и service_healthy, профили, include, develop.watch, !reset и !override и встроенные конфиги: семь возможностей Docker Compose v2, которые избавят compose.yaml от повторов и хрупких зависимостей.

Почти любой compose.yaml со временем превращается в длинную простыню из скопированных блоков, а сервисы стартуют в случайном порядке. Типичная картина: небольшой стек из Python-приложения, воркера, PostgreSQL и Nginx. Настройки повторяются в каждом сервисе, а после перезагрузки сервера приложение поднимается раньше базы и падает с ошибкой подключения, пока все не устаканится.

Обычно это лечат через sleep в скриптах запуска или через несколько почти одинаковых Compose-файлов, и оба способа быстро становятся сложнее самого стека. Между тем в Docker Compose v2 уже есть встроенные инструменты для этих задач: якоря YAML, healthcheck, профили, include, watch и другие. Многие из них появились или заметно расширились в версиях с 2.17 по 2.24, поэтому их легко пропустить.

Ниже семь приемов на примере одного стека. Все они входят в спецификацию Compose и не требуют сторонних плагинов.

Проверьте версию Compose

Убедитесь, что вы используете плагин Docker Compose v2 (команда docker compose через пробел), а не устаревший docker-compose. Для всех примеров нужна версия 2.24 или новее, а для приема с !reset и !override версия 2.24.4 или новее.

docker compose version
# Docker Compose version v2.39.2

1. Общие настройки через якоря YAML и поля x-

Самая частая проблема в разросшемся файле связана с повторами. Если web и worker используют одинаковую политику перезапуска и лимиты логов, любое изменение приходится вносить дважды. Вынесите общие настройки в поле расширения и подключайте их через якорь.

x-common: &common
  restart: unless-stopped
  logging:
    driver: json-file
    options:
      max-size: "10m"
      max-file: "3"

services:
  web:
    <<: *common
    build: .
    ports:
      - "8080:8000"
  worker:
    <<: *common
    build: .
    command: python worker.py

Поля, которые начинаются с x-, Compose игнорирует при создании сервисов, поэтому туда удобно складывать переиспользуемые блоки. Запись &common задает якорю имя, а <<: *common вливает его содержимое в сервис. Все, что указано прямо в сервисе, перекрывает общие значения. Заодно max-size и max-file ограничивают логи тремя файлами по 10 МБ и не дают контейнеру забить диск.

Проверить итоговую конфигурацию можно командой docker compose config: в выводе у обоих сервисов будут одинаковые restart и logging, хотя описаны они один раз.

2. Правильный порядок запуска через service_healthy

Обычный depends_on гарантирует только то, что контейнер базы запустится раньше приложения. Он не ждет, пока PostgreSQL начнет принимать подключения, поэтому приложение все равно может упасть с connection refused. Добавьте базе healthcheck и сделайте web зависимым от ее здорового состояния.

services:
  db:
    image: postgres:17
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      retries: 10
      start_period: 10s

  web:
    <<: *common
    build: .
    ports:
      - "8080:8000"
    depends_on:
      db:
        condition: service_healthy
        restart: true

Команда pg_isready сообщает об успехе, когда база готова к подключениям. Проверка идет каждые 5 секунд, после 10 неудач подряд контейнер считается нездоровым, а первые 10 секунд на инициализацию не учитываются. condition: service_healthy заставляет Compose запускать web только после успешной проверки, а restart: true перезапускает web, когда Compose явно перезапускает db. Пароль подставляется из файла .env в каталоге проекта.

docker compose up -d --wait

Флаг –wait возвращает управление только тогда, когда сервисы запущены и здоровы. Если проверка базы так и не пройдет, Compose сообщит об ошибке зависимости, и вместо бесконечного цикла падений приложения вы получите понятную причину.

3. Необязательные сервисы через профили

Некоторые сервисы нужны только иногда. Например, Adminer для отладки базы не обязательно держать запущенным постоянно. Привяжите его к профилю:

adminer:
    image: adminer
    ports:
      - "8081:8080"
    profiles: ["debug"]

Сервисы с неактивным профилем Compose пропускает, а сервисы без профиля запускаются всегда. Обычный docker compose up -d оставит Adminer выключенным, а когда он понадобится, достаточно включить профиль.

docker compose --profile debug up -d

# или через переменную окружения в .env
COMPOSE_PROFILES=debug

4. Разделение большого файла через include

Когда compose.yaml разрастается, в нем становится трудно ориентироваться. Элемент include позволяет вынести связанные сервисы в отдельные файлы, оставив их частью одного проекта. Например, перенесите базу в db/compose.yaml.

# db/compose.yaml
services:
  db:
    image: postgres:17
    env_file: db.env
    volumes:
      - ./data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      retries: 10
# compose.yaml
include:
  - db/compose.yaml

Пути внутри подключенного файла считаются относительно него самого, поэтому db.env и ./data указывают на файлы внутри каталога db. Зависимость depends_on в основном файле продолжает работать, потому что Compose собирает все в единую модель проекта. Если случайно оставить в основном файле второй сервис db, Compose сообщит о конфликте и не станет молча выбирать одну из версий.

5. Синхронизация кода через develop.watch

В разработке утомляет пересобирать образ после каждой правки. Compose Watch умеет синхронизировать локальные изменения прямо в работающий контейнер. Добавьте сервису web секцию develop:

develop:
      watch:
        - action: sync
          path: ./app
          target: /app
          ignore:
            - __pycache__/
        - action: rebuild
          path: requirements.txt

Действие sync копирует изменения из ./app в /app без пересборки образа, а кэш байткода Python исключается. Действие rebuild пересобирает образ при изменении requirements.txt, чтобы новые зависимости действительно установились. Запускается режим командой docker compose watch. Эта настройка рассчитана на разработку, на продакшен-серверах она не нужна.

6. Управление наследованием через !reset и !override

Для продакшена часто накладывают второй Compose-файл поверх базового. По умолчанию Compose объединяет значения, поэтому новый порт в override-файле добавится к старому, а не заменит его. Отсюда классическая загадка, почему удаленный порт все еще торчит наружу. Теги !override и !reset позволяют управлять этим явно.

# compose.prod.yaml
services:
  web:
    ports: !override
      - "127.0.0.1:8080:8000"
  adminer:
    ports: !reset []

!override полностью заменяет значение из базового файла: web будет слушать только 127.0.0.1:8080. !reset [] очищает унаследованное значение: у Adminer в продакшене не останется опубликованных портов. Проверить результат можно так:

docker compose -f compose.yaml -f compose.prod.yaml config

7. Встроенные конфиги через configs.content

Раз web теперь слушает только localhost, внешние запросы должен принимать обратный прокси. Конфигурацию Nginx не обязательно хранить отдельным файлом, ее можно встроить прямо в compose.yaml через верхнеуровневый элемент configs.

configs:
  nginx_conf:
    content: |
      server {
        listen 80;
        location / {
          proxy_pass http://web:8000;
          proxy_set_header Host $$host;
        }
      }

services:
  proxy:
    image: nginx:stable
    ports:
      - "80:80"
    configs:
      - source: nginx_conf
        target: /etc/nginx/conf.d/default.conf

Обратите внимание на $$host. Compose подставляет переменные и внутри content, поэтому обычный $host он воспринял бы как свою переменную и, скорее всего, заменил бы пустой строкой. Двойной знак доллара экранирует подстановку, и до Nginx доходит именно $host.

Как пользоваться этим безопасно

Когда итоговая конфигурация собирается из нескольких файлов, перед деплоем запускайте docker compose config. Команда проверит синтаксис и покажет модель, которую Compose будет использовать, так что неожиданные слияния и переопределения видны сразу. Учтите, что в ее выводе переменные окружения раскрываются открытым текстом, поэтому не выкладывайте его целиком в публичные обсуждения и тикеты.

Файлы с паролями закройте от посторонних и не коммитьте в репозиторий, а в .dockerignore исключите служебные каталоги, чтобы контекст сборки был меньше и образы собирались быстрее.

chmod 600 .env db/db.env

# .gitignore
.env
db/db.env

# .dockerignore
.git
db/data

Итог

Якоря и поля x- убирают повторы, healthcheck с service_healthy решает проблему порядка запуска, профили прячут вспомогательные сервисы, include делит большой проект на части, watch ускоряет разработку, !reset и !override дают точный контроль над слоями конфигурации, а configs.content избавляет от лишних мелких файлов. Все это уже есть в Compose v2, и ни один из приемов не требует дополнительных инструментов.

Источник: TecMint

Поделиться:

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *