M8: portal-shell unified frontend shell (Modular Monolith + micro-kernel). - Apollo Client -> apollo-router (port 4010, RSC prefetch) - 5 layouts: classic/focus/split/triple/canvas - Registry + PluginLoader (dynamic import ssr:false) - 3-layer props merge, Zustand PluginStore - 4 widgets: grades/notification-bell/user-menu/class-selector - config-service: new pluginConfig GraphQL resolver - apollo-router: CORS + header propagation for portal-shell - docker-compose.yml: portal-shell service block
447 lines
15 KiB
YAML
447 lines
15 KiB
YAML
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 链路核心(v2.1:Outbox 表监听 + 自动投递)
|
||
# 监听 MySQL binlog → 写入 Kafka topic
|
||
# v2.1 设计(ADR-032):业务代码只写 outbox 表,Debezium 监听 binlog 自动投递
|
||
# 废弃 OutboxPublisher 轮询线程(见 M0.5 / M8)
|
||
# 注册脚本:bash infra/debezium/register-outbox-connectors.sh
|
||
# ============================================================
|
||
debezium-connect:
|
||
image: quay.io/debezium/connect:2.7
|
||
container_name: edu-debezium
|
||
profiles: ["default", "p3", "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
|
||
config-service:
|
||
build:
|
||
context: ../services/config-service
|
||
container_name: edu-config-service
|
||
profiles: ["p3", "p4", "p5", "p6"]
|
||
restart: unless-stopped
|
||
environment:
|
||
DATABASE_URL: mysql://edu:${MYSQL_PASSWORD:-changeme}@edu-mysql:3306/next_edu_cloud
|
||
REDIS_URL: redis://edu-redis:6379
|
||
KAFKA_BROKERS: kafka:29092
|
||
ROUTER_AUTH_SECRET: ${ROUTER_AUTH_SECRET:-dev-router-secret}
|
||
DEV_MODE: ${DEV_MODE:-false}
|
||
PORT: "3011"
|
||
GRPC_PORT: "50059"
|
||
ports:
|
||
- "3011:3011"
|
||
- "50059:50059"
|
||
depends_on:
|
||
mysql:
|
||
condition: service_healthy
|
||
redis:
|
||
condition: service_started
|
||
healthcheck:
|
||
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3011/healthz"]
|
||
interval: 15s
|
||
timeout: 5s
|
||
retries: 5
|
||
apollo-router:
|
||
build:
|
||
context: ./apollo-router
|
||
container_name: edu-apollo-router
|
||
profiles: ["p3", "p4", "p5", "p6"]
|
||
restart: unless-stopped
|
||
environment:
|
||
ROUTER_AUTH_SECRET: ${ROUTER_AUTH_SECRET:-dev-router-secret}
|
||
APOLLO_ELV2_LICENSE: accept
|
||
ports:
|
||
- "3000:3000"
|
||
- "8088:8088"
|
||
depends_on:
|
||
- iam
|
||
- core-edu
|
||
- content
|
||
- msg
|
||
- ai
|
||
- data-ana
|
||
- config-service
|
||
healthcheck:
|
||
test: ["CMD", "curl", "-f", "http://localhost:8088/health"]
|
||
interval: 15s
|
||
timeout: 5s
|
||
retries: 5
|
||
# ============================================================
|
||
# portal-shell - 统一前端壳(v2.1 M8)
|
||
# Modular Monolith + 微内核架构,端口 4010(避开 4000-4003 portal 段)
|
||
# 查询走 apollo-router(M8 验收标准),非 gRPC 直连
|
||
# ============================================================
|
||
portal-shell:
|
||
build:
|
||
context: ..
|
||
dockerfile: apps/portal-shell/Dockerfile
|
||
container_name: edu-portal-shell
|
||
profiles: ["p3", "p4", "p5", "p6"]
|
||
restart: unless-stopped
|
||
environment:
|
||
NODE_ENV: production
|
||
NEXT_PUBLIC_APOLLO_ROUTER_URL: http://apollo-router:3000/graphql
|
||
APOLLO_ROUTER_URL: http://apollo-router:3000/graphql
|
||
NEXT_PUBLIC_API_GATEWAY_URL: http://api-gateway:8080
|
||
NEXT_PUBLIC_REALTIME_GATEWAY_URL: http://realtime-gateway:8081
|
||
NEXT_PUBLIC_DEV_MODE: "true"
|
||
ports:
|
||
- "4010:4010"
|
||
depends_on:
|
||
apollo-router:
|
||
condition: service_healthy
|
||
healthcheck:
|
||
test: ["CMD", "wget", "--spider", "-q", "http://localhost:4010/api/health"]
|
||
interval: 15s
|
||
timeout: 5s
|
||
retries: 5
|
||
start_period: 30s
|
||
# ============================================================
|
||
# Temporal Server - AI 工作流引擎(v2.1 §8.2 ADR-030)
|
||
# 仅用于 AI 耗时工作流 + Saga,CRUD 短事务禁止
|
||
# ============================================================
|
||
temporal-postgresql:
|
||
image: docker.m.daocloud.io/library/postgres:13
|
||
container_name: edu-temporal-postgres
|
||
profiles: ["p3", "p4", "p5", "p6"]
|
||
restart: unless-stopped
|
||
environment:
|
||
POSTGRES_USER: temporal
|
||
POSTGRES_PASSWORD: ${TEMPORAL_POSTGRES_PASSWORD:-temporal}
|
||
POSTGRES_DB: temporal
|
||
volumes:
|
||
- temporal_pg_data:/var/lib/postgresql/data
|
||
healthcheck:
|
||
test: ["CMD-SHELL", "pg_isready -U temporal"]
|
||
interval: 10s
|
||
timeout: 5s
|
||
retries: 5
|
||
temporal:
|
||
image: temporalio/auto-setup:1.23
|
||
container_name: edu-temporal
|
||
profiles: ["p3", "p4", "p5", "p6"]
|
||
restart: unless-stopped
|
||
depends_on:
|
||
temporal-postgresql:
|
||
condition: service_healthy
|
||
environment:
|
||
DBHOST: temporal-postgresql
|
||
DBPORT: 5432
|
||
DBUSER: temporal
|
||
DBPASSWORD: ${TEMPORAL_POSTGRES_PASSWORD:-temporal}
|
||
DBNAME: temporal
|
||
DB_PLUGIN: postgres
|
||
# 暴露 frontend gRPC 端口 7233 供 Worker 连接
|
||
SERVICES: "frontend,history,matching,worker"
|
||
ports:
|
||
- "7233:7233"
|
||
healthcheck:
|
||
test: ["CMD", "tctl", "--address", "localhost:7233", "cluster", "health"]
|
||
interval: 15s
|
||
timeout: 5s
|
||
start_period: 30s
|
||
retries: 10
|
||
temporal-ui:
|
||
image: temporalio/ui:2.30.0
|
||
container_name: edu-temporal-ui
|
||
profiles: ["p3", "p4", "p5", "p6"]
|
||
restart: unless-stopped
|
||
depends_on:
|
||
- temporal
|
||
environment:
|
||
TEMPORAL_ADDRESS: temporal:7233
|
||
TEMPORAL_CORS_ORIGINS: "http://localhost:4000,http://localhost:4001,http://localhost:4002,http://localhost:4003"
|
||
ports:
|
||
- "8085:8080"
|
||
volumes:
|
||
mysql_data:
|
||
redis_data:
|
||
clickhouse_data:
|
||
neo4j_data:
|
||
es_data:
|
||
grafana_data:
|
||
prometheus_data:
|
||
alertmanager_data:
|
||
loki_data:
|
||
temporal_pg_data:
|