Docker Compose глубже: profiles, healthcheck-зависимости, override

Рейтинг: 70.1% · 9 голосов
Практический курс по Docker: образы, контейнеры, тома, сети, Compose и продакшен. Уроки по главам с обсуждением.
Ответить
Аватара пользователя
Marina_DevOps
Сообщения: 54
Зарегистрирован: 11 май 2026, 05:31

Docker Compose глубже: profiles, healthcheck-зависимости, override

Сообщение Marina_DevOps »

Оглавление курса (46)
  1. Что такое Docker и какие задачи он решает
  2. Установка Docker и запуск первого контейнера
  3. Образы: слои, теги и реестр Docker Hub
  4. Пишем свой Dockerfile
  5. Тома и хранение данных: volumes и bind mounts
  6. Сети в Docker: связываем контейнеры между собой
  7. Переменные окружения и конфигурация контейнеров
  8. Docker Compose: поднимаем многоконтейнерное приложение
  9. Оптимизация образов: multi-stage сборка, размер и кэш слоёв
  10. Логи, отладка и мониторинг контейнеров
  11. Базовая безопасность контейнеров
  12. Подготовка к продакшену: что важно учесть
  13. Dockerfile глубже: ENTRYPOINT и CMD, HEALTHCHECK, .dockerignore, запуск не от root
  14. Реестры образов: приватные registry, push и pull, теги и digest, imagePullSecrets
  15. BuildKit и buildx: multi-arch сборки, секреты сборки, экспорт кэша
  16. Docker в CI/CD: автосборка, сканирование образов (Trivy, Docker Scout), публикация
  17. Итоговый проект и куда расти: от Dockerfile до прода, обзор оркестрации (Kubernetes, Podman, OCI)
  18. Архитектура Docker: dockerd, containerd, runc и OCI
  19. Как работает изоляция: namespaces и cgroups
  20. Образы изнутри: слои, OverlayFS и storage driver
  21. Жизненный цикл контейнера и политики перезапуска
  22. Отладка контейнеров: exec, attach, nsenter, debug
  23. Сети Docker глубже: bridge, overlay, macvlan и iptables
  24. Docker Compose глубже: profiles, healthcheck-зависимости, override (вы здесь)
  25. Данные и тома глубже: драйверы, бэкап, права
  26. Секреты и конфигурация: как не светить пароли
  27. Ограничение ресурсов: CPU, память, OOM и cgroups
  28. Здоровье и автозапуск: HEALTHCHECK, init и сигналы
  29. Логирование контейнеров: драйверы, ротация, сбор
  30. Мониторинг контейнеров: stats, cAdvisor, Prometheus
  31. Безопасность глубже: rootless, user namespaces, seccomp, capabilities
  32. Supply chain: сканирование, SBOM и подпись образов
  33. Глубокая оптимизация образов: distroless, scratch, кэш
  34. Multi-arch и сборка под ARM: buildx и эмуляция
  35. Реестры в продакшене: Harbor и облачные registry
  36. Docker без Docker: Podman, containerd, nerdctl, Buildah
  37. От Compose к оркестрации: когда контейнеров становится много
  38. Docker Swarm: встроенная оркестрация
  39. Сборка образов в CI без демона: kaniko, BuildKit, Buildah
  40. Траблшутинг Docker: типичные ошибки и как чинить
  41. Производительность и эксплуатация Docker-хоста
  42. Docker Desktop, WSL2 и альтернативы на Windows и Mac
  43. Контейнеризация приложений: бэкенд, фронтенд, БД
  44. Docker и Kubernetes: containerd, миграция, kompose
  45. Лучшие практики и антипаттерны Docker
  46. Сквозной проект: production-ready стек и путь дальше
Базовый docker compose ты уже умеешь: написал services, поднял compose up, всё взлетело. Но в реальном проекте появляются вопросы, на которые наивный compose отвечает плохо. Почему приложение падает при старте, хотя база "запущена"? Как держать один набор файлов и для прода, и для локалки, не разводя пять копий конфига? Как одной командой поднять только то, что нужно сейчас, а тяжёлый мониторинг оставить выключенным? Этот урок про профессиональный docker compose - тот, где ты управляешь зависимостями, средами и сборкой осознанно, а не молишься на удачный порядок запуска.

Разберём механику: depends_on с условием готовности, profiles, слияние нескольких файлов и override, YAML-якоря для DRY, интерполяцию переменных, именованные сети и тома, лимиты ресурсов и Compose Watch. И обязательно - где у Compose заканчиваются полномочия, и начинается территория оркестратора.

Почему depends_on врёт, и при чём тут healthcheck

Самая частая боль новичка: в compose.yaml написано, что app зависит от db, контейнеры стартуют в правильном порядке, а приложение всё равно падает с "connection refused". Дело в том, что обычный depends_on в форме списка гарантирует только порядок запуска контейнеров, а не готовность процесса внутри. Compose запустил контейнер с PostgreSQL - depends_on считает задачу выполненной. Но postgres внутри ещё инициализирует кластер, накатывает initdb, открывает сокет - это секунды, иногда десятки. Твой app в этот момент уже ломится за коннектом и получает отказ.

Решение - связка docker compose depends_on и docker compose healthcheck. Сначала описываешь healthcheck у зависимости: команда, которая возвращает 0, когда сервис реально готов принимать работу. Затем в depends_on задаёшь не список, а map с условием condition: service_healthy. Тогда Compose не просто запустит db первой, а будет ждать, пока healthcheck не перейдёт в состояние healthy, и только потом стартанёт app.

Код: Выделить всё

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: app
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres -d app"]
      interval: 5s
      timeout: 3s
      retries: 5
      start_period: 30s
    volumes:
      - pgdata:/var/lib/postgresql/data

  app:
    build: .
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started

  cache:
    image: redis:7
Разберём поля healthcheck, потому что почти все настраивают их неправильно. interval - как часто проверять. timeout - сколько ждём ответа одной проверки, после чего считаем её провалом. retries - сколько подряд провалов нужно, чтобы пометить контейнер unhealthy. А start_period - критически важное поле: это льготный период после старта, в течение которого провалы проверок НЕ считаются в счётчик retries и НЕ ведут к unhealthy. Без него тяжёлая база, которая поднимается 40 секунд, успеет насчитать retries провалов и слетит в unhealthy ещё до того, как реально запустится. Ставь start_period с запасом под худший холодный старт.

Условия depends_on бывают трёх видов. service_started - старое поведение, ждём только запуска контейнера. service_healthy - ждём healthy от healthcheck, то самое, что нам нужно для баз и брокеров. service_completed_successfully - ждём, пока контейнер отработает и выйдет с кодом 0; незаменимо для one-shot сервисов вроде миграций: app зависит от migrate с этим условием, и не стартует, пока схема не накатится.

Посмотрим, что Compose показывает при ожидании:

Код: Выделить всё

$ docker compose up
[+] Running 3/3
 - Container proj-db-1     Healthy   8.4s
 - Container proj-cache-1  Started   0.6s
 - Container proj-app-1    Started   8.7s
Видишь статус Healthy у db и время 8.4s - это Compose честно подождал healthcheck. Проверить состояние можно через docker inspect: в .State.Health.Status лежит starting, healthy или unhealthy, а в .State.Health.Log - история последних проверок с кодами выхода. Когда healthcheck флапает, первым делом смотри туда.

Важная оговорка: depends_on - инструкция уровня Compose, она работает при docker compose up. В голом Docker Swarm и тем более в Kubernetes её нет; там готовность решают через readiness-пробы и retry в самом приложении. Поэтому хорошая практика - даже с healthcheck закладывать в код переподключение к базе. Compose сглаживает старт, но не отменяет необходимость устойчивого приложения.

Изображение

Profiles: включаем сервисы по требованию

Типичная ситуация: в одном compose-файле живут и боевые сервисы, и вспомогательные - админка БД, отладочный прокси, нагрузочный генератор, сборщик метрик. Поднимать всё это сразу каждый раз не нужно и дорого. Тут на сцену выходит docker compose profiles.

Профиль - это метка на сервисе. Сервис без профилей запускается всегда. Сервис с профилем запускается только если этот профиль явно активирован. Активируешь через флаг --profile или переменную COMPOSE_PROFILES.

Код: Выделить всё

services:
  app:
    build: .
  db:
    image: postgres:16

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

  loadtest:
    image: grafana/k6
    profiles: ["perf"]

Код: Выделить всё

docker compose up                      # только app и db
docker compose --profile debug up      # app, db и adminer
COMPOSE_PROFILES=debug,perf docker compose up   # все четыре
Тонкость, на которой спотыкаются: если сервис без профиля зависит через depends_on от сервиса с профилем, Compose автоматически подтянет зависимость, даже если её профиль не активирован - иначе зависимость была бы битой. А вот если ты явно называешь сервис в команде - docker compose run adminer - его профиль активируется автоматически для этого запуска. Профили решают реальную задачу: один файл, один источник правды, но разные срезы стека под разные сценарии (dev, debug, ci, perf), без копипасты и без закомментированных блоков.

Несколько файлов, override и DRY: docker compose override

Главный антипаттерн - держать compose.prod.yaml и compose.dev.yaml как две полные копии, которые расходятся через неделю. Профессиональный подход - база плюс наложения. Тут работает docker compose override.

Compose по умолчанию ищет в рабочей директории два файла: compose.yaml и compose.override.yaml. Если оба есть, он автоматически сливает их в одну конфигурацию, причём значения из override имеют приоритет. Идея: в compose.yaml лежит общая правда, а в compose.override.yaml - локальные удобства разработчика (проброс портов, монтирование исходников, debug-переменные). override-файл обычно даже не коммитят или коммитят как пример - на проде его просто нет, и поднимается чистая база.

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

Код: Выделить всё

docker compose -f compose.yaml -f compose.prod.yaml up -d
Правила слияния надо знать точно, иначе будут сюрпризы. Скалярные значения (image, скаляр в environment, replicas) - последний файл побеждает, перезаписывает. Списки (ports, volumes, command-как-список) - конкатенируются, элементы добавляются, а не заменяются. Маппинги (environment в виде map, labels) - сливаются по ключам, при конфликте ключа побеждает значение из более позднего файла. Из-за того что списки добавляются, нельзя через override "убрать" порт - можно только добавить. Если нужно действительно заменить, а не дополнить, есть оператор !reset (обнулить поле) и !override (заменить значение целиком вместо слияния) - это спасает, когда базовый список мешает.

Для борьбы с повторами внутри одного файла есть два инструмента. YAML-якоря (anchors) - чистый синтаксис YAML: помечаешь блок &именем, переиспользуешь *именем, при необходимости расширяешь через <<: для слияния маппингов.

Код: Выделить всё

x-app-base: &app-base
  build: .
  environment: &app-env
    APP_ENV: production
    LOG_LEVEL: info
  restart: unless-stopped

services:
  web:
    <<: *app-base
    ports:
      - "8000:8000"
  worker:
    <<: *app-base
    command: ["python", "worker.py"]
Префикс x- в ключах верхнего уровня - это extension fields, кастомные секции, которые Compose игнорирует при валидации, но YAML видит, и якоря из них работают. Удобно складывать туда шаблоны. Второй инструмент, extends, позволяет наследовать конфигурацию сервиса даже из другого файла - мощнее якорей, но и сложнее в отладке, потому что источник наследования не виден в одном месте. Совет из практики: для DRY внутри файла бери якоря, для шаринга между файлами - аккуратный extends, а кардинальную разницу сред выноси в отдельные -f файлы.

Переменные, сети, тома и лимиты

Compose интерполирует переменные окружения прямо в YAML. Источник - окружение шелла и файл .env в директории проекта (не путать с env_file внутри сервиса - тот задаёт переменные внутри контейнера, а .env подставляет значения в сам compose-файл на этапе разбора). Поддерживается синтаксис со значениями по умолчанию: ${VAR:-default} подставит default, если VAR пуста или не задана; ${VAR-default} - только если не задана вообще; ${VAR:?error} - упадёт с сообщением, если переменной нет. Это спасает от молчаливых дыр в конфиге.

Код: Выделить всё

services:
  app:
    image: registry.example.ru/app:${APP_TAG:-latest}
    ports:
      - "${APP_PORT:?APP_PORT is required}:8000"
Сети и тома лучше объявлять явно и именованно, а не полагаться на дефолтную сеть проекта. Это даёт контроль над сегментацией: фронтовые сервисы в одной сети, бэкенд с базой - в изолированной, куда снаружи доступа нет.

Код: Выделить всё

services:
  app:
    networks: [frontend, backend]
  db:
    networks: [backend]

networks:
  frontend:
  backend:
  monitoring:
    external: true

volumes:
  pgdata:
Флаг external: true означает "эту сеть/том создаёт не Compose, она уже существует" - так подключаешься к ресурсу, который живёт вне жизненного цикла проекта (общий том логов, сеть между несколькими compose-проектами). Compose не будет её создавать и не удалит при down.

Масштабирование без правки файла - docker compose up --scale worker=4 поднимет четыре экземпляра worker. Работает только если у сервиса не зафиксирован контейнерный hostname или статический порт (иначе конфликт), поэтому масштабируемые сервисы делают за балансировщиком. Лимиты ресурсов задаются через deploy.resources - и вот важная деталь: блок deploy исторически был про Swarm, но docker compose up читает из него limits и reservations и применяет их к контейнеру, а вот replicas из deploy в обычном up игнорирует (для этого --scale).

Код: Выделить всё

services:
  app:
    deploy:
      resources:
        limits:
          cpus: "1.5"
          memory: 512M
        reservations:
          memory: 256M
    restart: unless-stopped
Политика restart: no (по умолчанию), always, on-failure, unless-stopped. Для боевых сервисов почти всегда unless-stopped - перезапускать при падении, но уважать ручную остановку.

Compose Watch и границы Compose в проде

Свежий инструмент для локальной разработки - Compose Watch, секция develop.watch. Он умнее, чем монтирование исходников томом, потому что различает типы изменений и реагирует разными действиями. Action sync копирует изменённые файлы в контейнер (горячая перезагрузка для интерпретируемых языков с hot reload). sync+restart синхронизирует и перезапускает контейнер - для случаев, когда правишь конфиг, который читается только на старте. rebuild пересобирает образ - когда меняется то, что влияет на сборку, например зависимости.

Код: Выделить всё

services:
  web:
    build: .
    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src
        - action: rebuild
          path: ./requirements.txt
        - action: sync+restart
          path: ./config
          target: /app/config
Запускаешь docker compose watch, и при сохранении файла в ./src он мгновенно улетает в контейнер, а при правке requirements.txt Compose сам пересоберёт образ. Это убирает классическую боль "поменял зависимость, забыл пересобрать, полчаса ловишь призрак".

Теперь честно про границы. docker compose - превосходный инструмент для локальной разработки, CI и небольших одно-хостовых деплоев. Но Compose работает в пределах одной машины. У него нет планировщика по кластеру, нет автоматического восстановления упавшего узла, нет rolling-update с проверкой готовности и автооткатом, нет горизонтального автоскейла. depends_on решает порядок старта, но не оркеструет жизнь сервисов в долгую. Поэтому в серьёзном проде Compose уступает место Kubernetes (или nomad, или managed-платформам). Это не недостаток Compose - это разделение труда: Compose для dev-цикла и простых стендов, оркестратор для отказоустойчивого продакшена. Понимать эту границу - признак зрелого инженера: не натягивать Compose на кластер и не тащить k8s туда, где хватает одного хоста.

Мини-лаба: собери всё вместе

Повтори руками, это закрепит механику.
  • Создай проект с тремя сервисами: db (postgres с healthcheck через pg_isready и start_period 30s), app (build, depends_on db с condition: service_healthy), и adminer под profiles: ["debug"].
  • Добавь one-shot сервис migrate, а в app пропиши depends_on migrate с condition: service_completed_successfully. Проверь, что app ждёт миграции.
  • Вынеси проброс портов и монтирование исходников в compose.override.yaml. Запусти без override (имитация прода) и с ним, сравни docker compose config - он покажет итоговую слитую конфигурацию.
  • Подними стек обычно и с --profile debug, убедись, что adminer появляется только во втором случае.
  • Добавь develop.watch с action sync на папку с кодом, запусти docker compose watch и поправь файл - проследи синхронизацию в логах.
  • Поиграй с ${APP_PORT:?...} - убери переменную из .env и посмотри, как Compose откажется стартовать с внятной ошибкой.
Контрольные вопросы
  • Чем depends_on в форме списка отличается от формы с condition: service_healthy, и зачем нужен start_period в healthcheck?
  • Как Compose сливает списки (ports) и маппинги (environment) при наложении нескольких -f файлов, и как принудительно заменить значение, а не дополнить?
  • Что произойдёт, если сервис без профиля зависит через depends_on от сервиса с профилем, который не активирован?
  • Когда выбрать action rebuild, а когда sync в Compose Watch, и почему монтирование тома их не заменяет?
Итог

Профессиональный docker compose - это управление готовностью, средами и сборкой, а не просто up. depends_on с service_healthy и грамотным healthcheck лечит гонки старта; profiles дают срезы стека из одного файла; override и -f-слияние держат среды в DRY без копипасты; якоря и extends убирают повторы; интерполяция с ${VAR:-default} делает конфиг гибким и явным; deploy.resources и restart готовят сервис к нагрузке; Compose Watch ускоряет dev-цикл. И главное - помни границу: Compose царит на одном хосте, а отказоустойчивый прод - территория оркестратора.
👍3 ❤️2 🔥1 😄 🤔1
Аватара пользователя
helsinge
Сообщения: 1
Зарегистрирован: 17 май 2026, 23:43

Re: Docker Compose глубже: profiles, healthcheck-зависимости, override

Сообщение helsinge »

Вот этот start_period меня и спасал - база поднималась 40 секунд и постоянно слетала в unhealthy, а я грешил на healthcheck. Спасибо, теперь понятно почему.
👍 ❤️ 🔥 😄 🤔
Аватара пользователя
arch7
Сообщения: 1
Зарегистрирован: 03 июн 2026, 00:17

Re: Docker Compose глубже: profiles, healthcheck-зависимости, override

Сообщение arch7 »

А правильно понимаю, что migrate с condition: service_completed_successfully это и есть нормальный способ накатывать миграции в compose, без хаков с entrypoint-скриптом который ждет базу?
👍2 ❤️ 🔥 😄 🤔1
Ответить
← Предыдущая глава
Сети Docker глубже: bridge, overlay, macvlan и iptables
Следующая глава →
Данные и тома глубже: драйверы, бэкап, права

Все главы курса «Docker: контейнеризация от основ до продакшена»

Поделиться темой: ✈ Telegram VK
Похожие запросы: как установить docker на ubuntu и linuxdocker команды шпаргалка для новичкаdocker run как запустить контейнер с флагамиdocker images как посмотреть и удалить образыdocker desktop и wsl2 на windows как настроитьdocker контейнер падает сразу после запуска как найти причину

Вернуться в «Docker с нуля»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость