Files
Edu/infra/docker-compose.deploy.yml
SpecialX 83b6919185 feat(api-gateway): retire teacher/student/parent-bff, route GraphQL to apollo-router
v2.1 M9+M10: BFF layer and old portals retired.

- api-gateway: remove TeacherBffURL/StudentBffURL/ParentBffURL config

- api-gateway: add ApolloRouterURL config and /api/graphql route

- api-gateway: /api/admin/graphql now proxies to apollo-router

- api-gateway: health checks now ping apollo-router instead of BFF

- deploy.yml: replace teacher-bff service block with apollo-router

- deploy.yml: add config-service service block (M3 dependency)

- deploy.yml: remove teacher-portal and admin-portal service blocks

- source code in services/{teacher,student,parent}-bff/ and apps/*-portal/ retained for rollback
2026-07-15 08:14:24 +08:00

373 lines
12 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.
# 服务器部署用 Docker Composeno-push 本地构建模式)
# 镜像来源CI 容器内本地 docker build不推送到 registry
# 基础设施MySQL + Redis 已在服务器 Docker 中运行(不在此文件管理)
#
# 部署目录:/opt/edu/
# 部署命令CI 自动执行):
# docker compose up -d --build --remove-orphans
#
# 首次部署手动步骤:
# 1. sudo mkdir -p /opt/edu && sudo chown -R $USER:$USER /opt/edu
# 2. cp infra/docker-compose.deploy.yml /opt/edu/docker-compose.yml
# 3. cp infra/deploy.env.example /opt/edu/.env && 编辑填入生产密钥
# 4. docker compose up -d --build
#
# 注意compose 文件中的 build.context 路径相对于 /opt/edu/ 目录
# CI 在 deploy 步骤会先把仓库 checkout 到 /opt/edu/repo/,再 cp compose 文件到 /opt/edu/
name: edu
services:
# ============================================================
# 应用服务10 个api-gateway + 3 Go/Python + 6 NestJS
# ============================================================
api-gateway:
build:
context: ./repo
dockerfile: services/api-gateway/Dockerfile
container_name: edu-api-gateway
restart: unless-stopped
environment:
API_GATEWAY_PORT: ${API_GATEWAY_PORT:-8080}
ENV: production
JWT_SECRET: ${JWT_SECRET}
JWT_ISSUER: ${JWT_ISSUER:-next-edu-cloud}
JWT_AUDIENCE: ${JWT_AUDIENCE:-next-edu-cloud}
# RS256 公钥端点(非 DevMode 下必填config.go W7 防护)
IAM_JWKS_URL: http://iam:3002/v1/iam/.well-known/jwks.json
# 生产环境强制关闭 dev-token 旁路
DEV_MODE: "false"
CLASSES_SERVICE_URL: http://classes:3001
IAM_SERVICE_URL: http://iam:3002
# v2.1 M9apollo-router 替代 teacher-bff / student-bff / parent-bff
APOLLO_ROUTER_URL: http://apollo-router:3000
CORE_EDU_SERVICE_URL: http://core-edu:3004
CONTENT_SERVICE_URL: http://content:3005
DATA_ANA_SERVICE_URL: http://data-ana:3006
MSG_SERVICE_URL: http://msg:3007
AI_SERVICE_URL: http://ai:3008
CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:3000,http://localhost:4000,http://localhost:4001,http://localhost:4002,http://localhost:4003,http://localhost:4010}
OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://otel-collector:4318}
LOG_LEVEL: ${LOG_LEVEL:-info}
ports:
- "${API_GATEWAY_PORT:-8080}:8080"
depends_on:
classes:
condition: service_healthy
apollo-router:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:8080/healthz"]
interval: 30s
timeout: 5s
start_period: 10s
retries: 3
networks:
- edu-net
- edu-shared
classes:
build:
context: ./repo
dockerfile: services/classes/Dockerfile
container_name: edu-classes
restart: unless-stopped
environment:
PORT: 3001
DATABASE_URL: ${DATABASE_URL}
REDIS_URL: ${REDIS_URL}
KAFKA_BROKERS: ${KAFKA_BROKERS:-}
OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://otel-collector:4318}
LOG_LEVEL: ${LOG_LEVEL:-info}
healthcheck:
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:3001/healthz"]
interval: 30s
timeout: 5s
start_period: 30s
retries: 5
networks:
- edu-net
- edu-shared
iam:
build:
context: ./repo
dockerfile: services/iam/Dockerfile
container_name: edu-iam
restart: unless-stopped
environment:
PORT: 3002
DATABASE_URL: ${DATABASE_URL}
REDIS_URL: ${REDIS_URL}
JWT_SECRET: ${JWT_SECRET}
JWT_ISSUER: ${JWT_ISSUER:-next-edu-cloud}
JWT_AUDIENCE: ${JWT_AUDIENCE:-next-edu-cloud}
OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://otel-collector:4318}
LOG_LEVEL: ${LOG_LEVEL:-info}
NODE_ENV: production
healthcheck:
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:3002/healthz"]
interval: 30s
timeout: 5s
start_period: 30s
retries: 5
networks:
- edu-net
- edu-shared
# v2.1 M3config-service 从 iam 拆出(插件配置 + 布局 + 用户偏好)
config-service:
build:
context: ./repo
dockerfile: services/config-service/Dockerfile
container_name: edu-config-service
restart: unless-stopped
environment:
PORT: 3011
GRPC_PORT: 50059
DATABASE_URL: ${DATABASE_URL}
REDIS_URL: ${REDIS_URL}
# GraphQL 子图信任凭证ADR-036
ROUTER_AUTH_SECRET: ${ROUTER_AUTH_SECRET:-edu-router-secret-change-in-production}
OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://otel-collector:4318}
LOG_LEVEL: ${LOG_LEVEL:-info}
NODE_ENV: production
depends_on:
iam:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:3011/healthz"]
interval: 30s
timeout: 5s
start_period: 30s
retries: 5
networks:
- edu-net
- edu-shared
# v2.1 M9teacher-bff / student-bff / parent-bff 已下线,由 apollo-router 替代
# 源码保留在 services/{teacher,student,parent}-bff/ 以便回滚
apollo-router:
build:
context: ./repo
dockerfile: infra/apollo-router/Dockerfile
container_name: edu-apollo-router
restart: unless-stopped
environment:
# 路由配置文件位于 /dist/router.yamlDockerfile COPY
APOLLO_ROUTER_CONFIG_PATH: /dist/router.yaml
# 信任凭证(子图 RouterAuthGuard 校验ADR-036
ROUTER_AUTH_SECRET: ${ROUTER_AUTH_SECRET:-edu-router-secret-change-in-production}
# 子图 URL 覆盖(通过 router.yaml override_subgraph_url 配置)
IAM_SUBGRAPH_URL: http://iam:3002/graphql
CONFIG_SERVICE_SUBGRAPH_URL: http://config-service:3011/graphql
CORE_EDU_SUBGRAPH_URL: http://core-edu:3004/graphql
CONTENT_SUBGRAPH_URL: http://content:3005/graphql
MSG_SUBGRAPH_URL: http://msg:3007/graphql
DATA_ANA_SUBGRAPH_URL: http://data-ana:3006/graphql
AI_SUBGRAPH_URL: http://ai:3008/graphql
ports:
- "${APOLLO_ROUTER_PORT:-3000}:3000"
depends_on:
iam:
condition: service_healthy
core-edu:
condition: service_healthy
content:
condition: service_healthy
msg:
condition: service_healthy
config-service:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:8088/healthz"]
interval: 30s
timeout: 5s
start_period: 10s
retries: 3
networks:
- edu-net
- edu-shared
core-edu:
build:
context: ./repo
dockerfile: services/core-edu/Dockerfile
container_name: edu-core-edu
restart: unless-stopped
environment:
PORT: 3004
DATABASE_URL: ${DATABASE_URL}
REDIS_URL: ${REDIS_URL}
KAFKA_BROKERS: ${KAFKA_BROKERS:-}
OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://otel-collector:4318}
LOG_LEVEL: ${LOG_LEVEL:-info}
NODE_ENV: production
DEV_MODE: "false"
healthcheck:
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:3004/healthz"]
interval: 30s
timeout: 5s
start_period: 30s
retries: 5
networks:
- edu-net
- edu-shared
content:
build:
context: ./repo
dockerfile: services/content/Dockerfile
container_name: edu-content
restart: unless-stopped
environment:
PORT: 3005
DATABASE_URL: ${DATABASE_URL}
REDIS_URL: ${REDIS_URL}
NEO4J_URL: ${NEO4J_URL:-}
NEO4J_PASSWORD: ${NEO4J_PASSWORD:-}
ES_URL: ${ES_URL:-}
OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://otel-collector:4318}
LOG_LEVEL: ${LOG_LEVEL:-info}
NODE_ENV: production
DEV_MODE: "false"
healthcheck:
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:3005/healthz"]
interval: 30s
timeout: 5s
start_period: 30s
retries: 5
networks:
- edu-net
- edu-shared
msg:
build:
context: ./repo
dockerfile: services/msg/Dockerfile
container_name: edu-msg
restart: unless-stopped
environment:
PORT: 3007
DATABASE_URL: ${DATABASE_URL}
REDIS_URL: ${REDIS_URL}
KAFKA_BROKERS: ${KAFKA_BROKERS:-}
ES_URL: ${ES_URL:-}
PUSH_GATEWAY_URL: http://push-gateway:8081
OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://otel-collector:4318}
LOG_LEVEL: ${LOG_LEVEL:-info}
NODE_ENV: production
DEV_MODE: "false"
depends_on:
push-gateway:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:3007/healthz"]
interval: 30s
timeout: 5s
start_period: 30s
retries: 5
networks:
- edu-net
- edu-shared
ai:
build:
context: ./repo/services/ai
dockerfile: Dockerfile
container_name: edu-ai
restart: unless-stopped
environment:
PORT: 3008
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
OPENAI_BASE_URL: ${OPENAI_BASE_URL:-https://api.openai.com/v1}
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
OTEL_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://otel-collector:4318}
LOG_LEVEL: ${LOG_LEVEL:-info}
DEV_MODE: "false"
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:3008/healthz')"]
interval: 30s
timeout: 5s
start_period: 20s
retries: 3
networks:
- edu-net
- edu-shared
data-ana:
build:
context: ./repo/services/data-ana
dockerfile: Dockerfile
container_name: edu-data-ana
restart: unless-stopped
environment:
PORT: 3006
CLICKHOUSE_HOST: ${CLICKHOUSE_HOST:-}
CLICKHOUSE_PORT: ${CLICKHOUSE_PORT:-8123}
CLICKHOUSE_DATABASE: ${CLICKHOUSE_DATABASE:-edu_analytics}
CLICKHOUSE_USER: ${CLICKHOUSE_USER:-}
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-}
OTEL_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://otel-collector:4318}
LOG_LEVEL: ${LOG_LEVEL:-info}
DEV_MODE: "false"
KAFKA_BROKERS: ${KAFKA_BROKERS:-}
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:3006/healthz')"]
interval: 30s
timeout: 5s
start_period: 20s
retries: 3
networks:
- edu-net
- edu-shared
push-gateway:
build:
context: ./repo/services/push-gateway
dockerfile: Dockerfile
container_name: edu-push-gateway
restart: unless-stopped
environment:
PUSH_GATEWAY_PORT: 8081
JWT_SECRET: ${JWT_SECRET}
# M7 (ADR-040): Redis Pub/Sub backplane for real-time push.
# push-gateway no longer mounts Kafka directly.
REDIS_URL: ${REDIS_URL}
# PUSH_INTERNAL_TOKEN is the canonical env var (ARB-013 alignment with
# msg). INTERNAL_API_TOKEN kept as backward-compat alias.
PUSH_INTERNAL_TOKEN: ${PUSH_INTERNAL_TOKEN:-${INTERNAL_API_TOKEN:-edu-internal-token}}
JWKS_URL: http://iam:3002/v1/iam/.well-known/jwks.json
WS_ALLOWED_ORIGINS: ${WS_ALLOWED_ORIGINS:-http://localhost:3000,http://localhost:4000,http://localhost:4001,http://localhost:4002,http://localhost:4003}
MAX_CONNS_PER_USER: "5"
HEARTBEAT_INTERVAL_SECONDS: "30"
OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://otel-collector:4318}
DEV_MODE: "false"
ports:
- "${PUSH_GATEWAY_PORT:-8081}:8081"
healthcheck:
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:8081/healthz"]
interval: 30s
timeout: 5s
start_period: 10s
retries: 3
networks:
- edu-net
- edu-shared
# v2.1 M10teacher-portal / admin-portal 已下线,由 portal-shell 替代
# 源码保留在 apps/{teacher,student,parent,admin}-portal/ 以便回滚
# portal-shell 部署在 infra/docker-compose.yml开发环境端口 4010
networks:
# 应用服务内部网络
edu-net:
driver: bridge
# 与已有 MySQL/Redis 共享的网络
# 需确保 MySQL/Redis 容器已加入名为 edu-shared 的网络:
# docker network create edu-shared (若不存在)
# docker network connect edu-shared edu-mysql
# docker network connect edu-shared edu-redis
edu-shared:
external: true