name: edu-full
services:
  mysql:
    # 镜像加速：通过 daocloud 镜像源绕过 docker.io 被墙问题
    image: docker.m.daocloud.io/library/mysql:8.0
    container_name: edu-mysql
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-changeme}
      MYSQL_DATABASE: ${MYSQL_DATABASE:-next_edu_cloud}
      MYSQL_USER: ${MYSQL_USER:-edu}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD:-changeme}
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql
      - ./init-sql:/docker-entrypoint-initdb.d:ro
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5
  redis:
    image: docker.m.daocloud.io/library/redis:7-alpine
    container_name: edu-redis
    restart: unless-stopped
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 5
  kafka:
    image: docker.m.daocloud.io/confluentinc/cp-kafka:7.6.0
    container_name: edu-kafka
    profiles: ["p3", "p4", "p5", "p6"]
    restart: unless-stopped
    depends_on:
      zookeeper:
        condition: service_healthy
    environment:
      KAFKA_BROKER_ID: 1
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      # 双 listener：INSIDE 容器间互访（kafka:29092），OUTSIDE 主机访问（localhost:9092）
      # 必须用 INSIDE 作为 inter.broker.listener.name，否则 Debezium Connect 拿到 metadata
      # 后会切回 advertised.listeners 中的 localhost，导致连接失败
      KAFKA_LISTENERS: INSIDE://:29092,OUTSIDE://:9092
      KAFKA_ADVERTISED_LISTENERS: INSIDE://kafka:29092,OUTSIDE://localhost:9092
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INSIDE:PLAINTEXT,OUTSIDE:PLAINTEXT
      KAFKA_INTER_BROKER_LISTENER_NAME: INSIDE
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
      KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true"
    ports:
      - "9092:9092"
    healthcheck:
      test: ["CMD", "kafka-topics", "--bootstrap-server", "localhost:9092", "--list"]
      interval: 15s
      timeout: 10s
      retries: 5
  zookeeper:
    image: docker.m.daocloud.io/confluentinc/cp-zookeeper:7.6.0
    container_name: edu-zookeeper
    profiles: ["p3", "p4", "p5", "p6"]
    restart: unless-stopped
    environment:
      ZOOKEEPER_CLIENT_PORT: 2181
    healthcheck:
      test: ["CMD", "nc", "-z", "localhost", "2181"]
      interval: 10s
      timeout: 5s
      retries: 5
  clickhouse:
    image: docker.m.daocloud.io/clickhouse/clickhouse-server:24.3
    container_name: edu-clickhouse
    profiles: ["p4", "p5", "p6"]
    restart: unless-stopped
    ports:
      - "8123:8123"
      - "9000:9000"
    volumes:
      - clickhouse_data:/var/lib/clickhouse
      # 覆盖默认 default-user.xml 限制（默认仅允许 127.0.0.1/::1 无密码访问）
      - ./clickhouse/users.d/custom-users.xml:/etc/clickhouse-server/users.d/custom-users.xml:ro
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://localhost:8123/ping"]
      interval: 10s
      timeout: 5s
      retries: 5
  neo4j:
    image: docker.m.daocloud.io/library/neo4j:5.20
    container_name: edu-neo4j
    profiles: ["p4", "p5", "p6"]
    restart: unless-stopped
    environment:
      NEO4J_AUTH: neo4j/${NEO4J_PASSWORD:-changeme}
    ports:
      - "7474:7474"
      - "7687:7687"
    volumes:
      - neo4j_data:/data
    healthcheck:
      test: ["CMD", "cypher-shell", "-u", "neo4j", "-p", "${NEO4J_PASSWORD:-changeme}", "RETURN 1"]
      interval: 15s
      timeout: 10s
      retries: 5
  elasticsearch:
    # Elastic 官方镜像在 docker.elastic.co，非 Docker Hub，通常不被墙
    image: docker.elastic.co/elasticsearch/elasticsearch:8.13.0
    container_name: edu-es
    profiles: ["p5", "p6"]
    restart: unless-stopped
    environment:
      discovery.type: single-node
      xpack.security.enabled: "false"
      ES_JAVA_OPTS: "-Xms512m -Xmx512m"
    ports:
      - "9200:9200"
    volumes:
      - es_data:/usr/share/elasticsearch/data
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9200/_cluster/health"]
      interval: 15s
      timeout: 10s
      retries: 5
  jaeger:
    image: docker.m.daocloud.io/jaegertracing/all-in-one:1.57
    container_name: edu-jaeger
    profiles: ["observability"]
    restart: unless-stopped
    environment:
      COLLECTOR_OTLP_ENABLED: "true"
    ports:
      - "16686:16686"
      - "4318:4318"
  # ============================================================
  # Debezium Connect - CDC 链路核心
  # 监听 MySQL binlog → 写入 Kafka topic
  # topic 命名约定：<prefix>.<database>.<table>（如 edu-cdc.next_edu_cloud.grades）
  # ============================================================
  debezium-connect:
    image: quay.io/debezium/connect:2.7
    container_name: edu-debezium
    profiles: ["p4", "p5", "p6"]
    restart: unless-stopped
    depends_on:
      kafka:
        condition: service_healthy
    environment:
      # Kafka Connect 基础配置（Debezium 2.x 容器映射规则：环境变量名大写 → connect 配置项）
      # 必须用 INSIDE listener (kafka:29092)，否则会拿到 OUTSIDE 的 localhost metadata 导致连不上
      BOOTSTRAP_SERVERS: kafka:29092
      GROUP_ID: edu-debezium
      CONFIG_STORAGE_TOPIC: edu-connect-configs
      OFFSET_STORAGE_TOPIC: edu-connect-offsets
      STATUS_STORAGE_TOPIC: edu-connect-status
      # 内部 converter 配置（必须与 Debezium 事件格式一致）
      CONFIG_STORAGE_REPLICATION_FACTOR: "1"
      OFFSET_STORAGE_REPLICATION_FACTOR: "1"
      STATUS_STORAGE_REPLICATION_FACTOR: "1"
      KEY_CONVERTER: org.apache.kafka.connect.json.JsonConverter
      VALUE_CONVERTER: org.apache.kafka.connect.json.JsonConverter
      KEY_CONVERTER_SCHEMAS_ENABLE: "false"
      VALUE_CONVERTER_SCHEMAS_ENABLE: "false"
      # 监听端口
      REST_PORT: 8083
      REST_ADVERTISED_HOST_NAME: debezium-connect
      # 日志级别
      LOG_LEVEL: INFO
    ports:
      - "8083:8083"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8083/connectors"]
      interval: 15s
      timeout: 5s
      start_period: 30s
      retries: 10
    networks:
      - default
  prometheus:
    image: docker.m.daocloud.io/prom/prometheus:v2.51.0
    container_name: edu-prometheus
    profiles: ["observability"]
    restart: unless-stopped
    command:
      - "--config.file=/etc/prometheus/prometheus.yml"
      - "--storage.tsdb.path=/prometheus"
      - "--storage.tsdb.retention.time=15d"
      - "--web.enable-lifecycle"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prometheus_data:/prometheus
    ports:
      - "9090:9090"
  grafana:
    image: docker.m.daocloud.io/grafana/grafana:10.4.0
    container_name: edu-grafana
    profiles: ["observability"]
    restart: unless-stopped
    environment:
      GF_SECURITY_ADMIN_PASSWORD: admin
    ports:
      - "3030:3000"
    volumes:
      - grafana_data:/var/lib/grafana
  # ============================================================
  # Alertmanager - 告警路由与抑制（observability profile）
  # ============================================================
  alertmanager:
    image: docker.m.daocloud.io/prom/alertmanager:v0.27.0
    container_name: edu-alertmanager
    profiles: ["observability"]
    restart: unless-stopped
    command:
      - "--config.file=/etc/alertmanager/alertmanager.yml"
      - "--storage.path=/alertmanager"
    ports:
      - "9093:9093"
    volumes:
      - ./alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
      - alertmanager_data:/alertmanager
  # ============================================================
  # Loki - 日志聚合（observability profile）
  # ============================================================
  loki:
    image: docker.m.daocloud.io/grafana/loki:3.2.1
    container_name: edu-loki
    profiles: ["observability"]
    restart: unless-stopped
    command: -config.file=/etc/loki/local-config.yaml
    ports:
      - "3100:3100"
    volumes:
      - loki_data:/loki
  # ============================================================
  # Promtail - 日志采集（observability profile）
  # ============================================================
  promtail:
    image: docker.m.daocloud.io/grafana/promtail:3.2.1
    container_name: edu-promtail
    profiles: ["observability"]
    restart: unless-stopped
    command: -config.file=/etc/promtail/config.yml
    volumes:
      - ./promtail/config.yml:/etc/promtail/config.yml:ro
      - /var/lib/docker/containers:/var/lib/docker/containers:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
    depends_on:
      - loki
  # ============================================================
  # Exporters（observability profile，与 Prometheus 同网络）
  # ============================================================
  node-exporter:
    image: docker.m.daocloud.io/prom/node-exporter:v1.8.2
    container_name: edu-node-exporter
    profiles: ["observability"]
    restart: unless-stopped
    command:
      - "--path.rootfs=/host"
    ports:
      - "9100:9100"
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/host:ro
  mysql-exporter:
    image: docker.m.daocloud.io/prom/mysqld-exporter:v0.15.1
    container_name: edu-mysql-exporter
    profiles: ["observability"]
    restart: unless-stopped
    command:
      - "--mysqld.address=edu-mysql:3306"
      - "--mysqld.username=edu:changeme"
    environment:
      MYSQLD_EXPORTER_PASSWORD: "changeme"
    ports:
      - "9104:9104"
    depends_on:
      mysql:
        condition: service_healthy
  redis-exporter:
    image: docker.m.daocloud.io/oliver006/redis_exporter:v1.67.0
    container_name: edu-redis-exporter
    profiles: ["observability"]
    restart: unless-stopped
    environment:
      REDIS_ADDR: "redis://edu-redis:6379"
    ports:
      - "9121:9121"
    depends_on:
      redis:
        condition: service_started
volumes:
  mysql_data:
  redis_data:
  clickhouse_data:
  neo4j_data:
  es_data:
  grafana_data:
  prometheus_data:
  alertmanager_data:
  loki_data:
