Learning Platform
Глоссарий Troubleshooting
Урок 18.05 · 24 мин
Средний
dockerdata-engineeringcomposepatternsproduction

Паттерны компоновки DE-стека

В прошлых четырёх уроках мы поднимали изолированные стенды: Airflow отдельно, Kafka отдельно, Spark отдельно, ClickHouse отдельно. На практике DE работает со всеми этими сервисами одновременно: Airflow дёргает Spark, Spark пишет в ClickHouse, ClickHouse подписан на Kafka. Один стек.

Этот урок — про паттерны компоновки: как соединить компоненты в один проект без хаоса в YAML. Темы: общая сеть, healthcheck-driven startup, secrets через .env, multi-file compose и profiles для опциональных сервисов.


Switches vs Hubs — как коммутатор знает куда слать фрейм

Паттерн 1: общая Docker-сеть

Самый частый вопрос junior’а: “Как Airflow подключится к моему Postgres?”. Ответ — общая сеть.

В compose есть три уровня сетей:

  • Default — compose автоматически создаёт сеть {project}_default, если ты не указал networks. Все сервисы там.
  • Named in-project — ты создаёшь networks: { de-net: {} } и привязываешь сервисы. Изоляция от других compose-проектов.
  • External — сеть, созданная вне compose (docker network create de-shared). Несколько compose-проектов могут к ней подключиться.

Для DE-стека правильный выбор — named сеть в одном compose, или external для multi-file подхода.

# Опция А: всё в одном compose.yml, одна named-сеть

networks:
  de-net:
    driver: bridge

services:
  postgres: { networks: [de-net] }
  airflow:  { networks: [de-net] }
  kafka:    { networks: [de-net] }
  clickhouse: { networks: [de-net] }
# Опция Б: разные compose-файлы делят external-сеть

# Сначала создать руками:
# docker network create de-shared

# В compose.airflow.yml:
networks:
  default:
    name: de-shared
    external: true

# В compose.kafka.yml:
networks:
  default:
    name: de-shared
    external: true
Один проект vs multi-file через external network
Option A: один compose.ymlall services in one fileПростая модель. Один YAML, одна сеть, одна команда docker compose up. Минус: файл становится огромным (200+ строк) при 10+ сервисах.
Option B: multi-fileexternal networkРазные compose-файлы для логических групп (airflow, kafka, observability). Делят external network. Можно поднимать/опускать независимо.
A: один updocker compose up -- запускает всё одной командой.
B: независимые updocker compose -f compose.airflow.yml up -- только Airflow. И отдельно kafka, отдельно clickhouse.

Паттерн 2: healthcheck-driven startup

Один из самых раздражающих junior-багов: “у меня Airflow упал при старте, потому что Postgres ещё не успел подняться”. Решается через healthcheck + depends_on с conditions.

Compose v2 умеет:

  • service_started — стартовал процесс (быстрый, ненадёжный).
  • service_healthy — healthcheck вернул успех.
  • service_completed_successfully — контейнер завершился с exit 0 (для init-job’ов).
services:
  postgres:
    image: postgres:16
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "airflow"]
      interval: 5s
      retries: 10

  airflow-init:
    image: apache/airflow:2.10.3
    depends_on:
      postgres:
        condition: service_healthy   # ждёт, пока pg_isready
    command: airflow db migrate

  airflow-webserver:
    image: apache/airflow:2.10.3
    depends_on:
      airflow-init:
        condition: service_completed_successfully   # ждёт exit 0
Цепочка startup: postgres -> init -> webserver+scheduler
1. postgresPostgres. Healthcheck pg_isready: возвращает 0, когда БД принимает соединения. Обычно 3-8 секунд после start.
healthy
2. initairflow-init. Делает db migrate, создаёт users. Завершается с exit 0. Если ошибка -- exit != 0, и dependent сервисы не стартуют.
3a. webserverwebserver. Стартует после init. Health -- curl /health.
3b. schedulerscheduler. Стартует параллельно с webserver после init.

Без healthcheck’ов depends_on гарантирует только порядок старта контейнеров, но не готовность сервиса. Postgres может запустить postgres процесс, но ещё не принимать соединения. Airflow попробует подключиться и упадёт.

Правило: для каждого критичного сервиса (БД, message broker, key-value store) пиши healthcheck. Это 3 строчки YAML, которые экономят часы debug’а.


Паттерн 3: secrets через .env

Никогда не пиши пароли в compose.yml. Используй .env:

# .env (в gitignore!)
POSTGRES_PASSWORD=airflow-secret-2026
AIRFLOW_FERNET_KEY=fa6sFvHkjhdGFsa8jdhgFKJhg23dfgKJh==
KAFKA_BOOTSTRAP=kafka:9092
CLICKHOUSE_PASSWORD=ch-strong-pass
# compose.yml
services:
  postgres:
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
  airflow-webserver:
    environment:
      AIRFLOW__CORE__FERNET_KEY: ${AIRFLOW_FERNET_KEY}
      AIRFLOW__DATABASE__SQL_ALCHEMY_CONN: postgresql://airflow:${POSTGRES_PASSWORD}@postgres/airflow

Compose автоматически подхватывает .env из директории, где запускается docker compose. Переменные подставляются при парсинге YAML.

WARNING

ВНИМАНИЕ: добавь .env в .gitignore СРАЗУ при создании репо. Если случайно закоммитил пароль — git rm и переустановить пароль (старый уже скомпрометирован, история git публична). Создай .env.example с пустыми значениями для документации.

Для production-секретов (не dev) есть secrets: в compose v3 — это файлы, которые монтируются как /run/secrets/{name} внутри контейнера. Но для compose-стенда .env обычно достаточно.


Паттерн 4: multi-file compose

Когда compose.yml перевалил за 300 строк — разбивай. Compose поддерживает множество файлов:

docker compose -f compose.yml -f compose.kafka.yml -f compose.observability.yml up -d

Структура репо:

project/
├── docker/
│   ├── compose.yml              # ядро: airflow, postgres
│   ├── compose.kafka.yml        # kafka + kafka-ui
│   ├── compose.spark.yml        # spark-master + workers
│   ├── compose.clickhouse.yml   # clickhouse
│   └── compose.observability.yml # prometheus + grafana
├── .env
├── dags/
├── data/
└── Makefile

Compose сливает все указанные файлы в один effective compose. Service с одинаковым именем мерджится по правилам YAML (последний выигрывает для scalars, list/dict — пополняется).

Удобно сделать Makefile-аналог:

COMPOSE_FILES = -f compose.yml -f compose.kafka.yml -f compose.clickhouse.yml

up:
	docker compose ${COMPOSE_FILES} up -d

down:
	docker compose ${COMPOSE_FILES} down

logs:
	docker compose ${COMPOSE_FILES} logs -f

И теперь просто make up / make down / make logs.


Паттерн 5: profiles для опциональных сервисов

В одном compose-файле можно описать профили — группы сервисов, которые стартуют только при явном опт-ине. Идеально для опциональных сервисов: spark-cluster нужен не каждый день, observability — для продвинутого debug.

services:
  postgres:
    image: postgres:16
    # без profile -- всегда поднимается

  airflow-webserver:
    image: apache/airflow:2.10.3
    # без profile -- всегда поднимается

  spark-master:
    image: bitnami/spark:3.5
    profiles: ["spark"]   # только если --profile spark

  spark-worker-1:
    image: bitnami/spark:3.5
    profiles: ["spark"]

  prometheus:
    image: prom/prometheus
    profiles: ["observability"]

  grafana:
    image: grafana/grafana
    profiles: ["observability"]

Запуск:

# Только ядро
docker compose up -d
# postgres + airflow

# С Spark
docker compose --profile spark up -d
# postgres + airflow + spark-master + spark-worker

# Со всем
docker compose --profile spark --profile observability up -d

# Несколько профилей через переменную окружения
COMPOSE_PROFILES=spark,observability docker compose up -d
TIP

Профили — киллер-фича для большого стека. У тебя один compose.yml на ~500 строк, но ты включаешь только нужные слои. Если работаешь над DAG’ом, который не трогает Spark — не поднимай 2 worker’а, экономь RAM и CPU.


Куда складывать compose-файлы в репо

Стандартная convention:

project/
├── docker/                  # ВСЕ compose-файлы и Dockerfile'ы тут
│   ├── compose.yml
│   ├── compose.spark.yml
│   ├── airflow/
│   │   └── Dockerfile       # Custom Airflow image (с твоими providers)
│   └── producer/
│       └── Dockerfile
├── dags/                    # Airflow DAG-и
├── notebooks/               # Jupyter (если нужны)
├── sql/                     # SQL-миграции, init-скрипты
├── tests/                   # pytest-тесты
├── .env.example             # пример .env (без секретов)
├── .env                     # реальный .env (в .gitignore)
└── Makefile                 # обёртки над docker compose

Почему docker/:

  • Отделяет инфраструктуру от кода приложения.
  • Один взгляд — и понятно, где compose, где Dockerfile’ы.
  • Можно делать relative path в compose: build: ./airflow (соседняя директория).

В крупных проектах часто появляется infra/ или deploy/ — то же самое, просто другое имя.


Локальный override (.override.yml)

Compose автоматически подхватывает compose.override.yml, если он есть в той же директории. Это идеально для локальных кастомизаций, которые не должны попасть в git:

# compose.override.yml (в .gitignore)
services:
  airflow-webserver:
    ports:
      - "8081:8080"   # у меня 8080 занят другим проектом, мап на 8081

  postgres:
    ports:
      - "5433:5432"   # хочу подключиться pgAdmin'ом с хоста

docker compose up без флагов сольёт compose.yml + compose.override.yml. Команда docker compose -f compose.yml up (явный -f) НЕ подхватит override — только базовый.


Минимальный шаблон DE-стенда

Соберём всё в один компактный пример:

# compose.yml
networks:
  de-net:
    driver: bridge

services:
  postgres:
    image: postgres:16
    environment:
      POSTGRES_USER: airflow
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: airflow
    volumes:
      - pg-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "airflow"]
      interval: 5s
    networks: [de-net]

  airflow-webserver:
    image: apache/airflow:2.10.3
    environment:
      AIRFLOW__DATABASE__SQL_ALCHEMY_CONN: postgresql://airflow:${POSTGRES_PASSWORD}@postgres/airflow
    ports:
      - "8080:8080"
    depends_on:
      postgres:
        condition: service_healthy
    networks: [de-net]
    command: webserver

  kafka:
    image: bitnami/kafka:3.8
    profiles: ["streaming"]
    environment:
      KAFKA_CFG_NODE_ID: 1
      KAFKA_CFG_PROCESS_ROLES: controller,broker
      # ... остальные KRaft-настройки
    networks: [de-net]

  clickhouse:
    image: clickhouse/clickhouse-server:24.10
    profiles: ["streaming"]
    ulimits:
      nofile: { soft: 262144, hard: 262144 }
    networks: [de-net]

volumes:
  pg-data:

И workflow:

# Базовое: только Airflow + Postgres
docker compose up -d

# Стриминговый стек: добавляются Kafka + ClickHouse
docker compose --profile streaming up -d

# Stop streaming, оставить Airflow
docker compose --profile streaming down
docker compose up -d  # Airflow остался live

Попробуй сам

# 1. Создай .env с твоими паролями
cat > .env <<EOF
POSTGRES_PASSWORD=secret123
CLICKHOUSE_PASSWORD=ch-pass
EOF

# 2. Скопируй шаблон выше в compose.yml

# 3. Базовый запуск
docker compose up -d
docker compose ps   # postgres + airflow

# 4. Запуск со streaming profile
docker compose --profile streaming up -d
docker compose ps   # + kafka + clickhouse

# 5. Создай compose.override.yml для локальной кастомизации:
cat > compose.override.yml <<EOF
services:
  airflow-webserver:
    ports:
      - "8088:8080"
EOF
docker compose up -d   # webserver теперь на :8088

# 6. Multi-file (для теста)
docker compose -f compose.yml -f compose.override.yml config
# Покажет effective compose

# 7. Cleanup
docker compose --profile streaming down -v

Проверка знанийKnowledge check
Ты сделал compose.yml с airflow и kafka. Airflow стартует и сразу пытается подключиться к kafka:9092, но получает Connection refused -- хотя kafka контейнер UP. Через 30 секунд kafka становится готова. Как починить, чтобы airflow дожидался Kafka?
ОтветAnswer
Нужны два элемента. (1) Healthcheck для Kafka: например, kafka-topics.sh --bootstrap-server localhost:9092 --list возвращает exit 0 только когда broker реально готов. (2) depends_on в airflow со condition: service_healthy. Без healthcheck depends_on гарантирует только порядок старта контейнеров, не готовность процесса. Альтернативный (худший) вариант -- retry-логика в самом приложении (Airflow operator с retries=5). Healthcheck правильнее: меньше cascading failures, лучшее наблюдение в docker compose ps.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Чем external networks отличаются от обычных named networks в compose?

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

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

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

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