Files
Edu/infra/docker-compose.yml
SpecialX 514e26ebb4 feat(portal-shell): implement portal-shell with apollo-router integration
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
2026-07-15 08:06:09 +08:00

447 lines
15 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
# 双 listenerINSIDE 容器间互访kafka:29092OUTSIDE 主机访问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.1Outbox 表监听 + 自动投递)
# 监听 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
# ============================================================
# Exportersobservability 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-routerM8 验收标准),非 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 耗时工作流 + SagaCRUD 短事务禁止
# ============================================================
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: