chore(infra): docker dev-apps stack + apollo-router v1.45 compat

- 新增 docker-compose.dev-apps.yml:7 个应用服务容器化
  (iam/classes/core-edu/content/msg/push-gateway/api-gateway)
  + apollo-router override
- 新增 apollo-router dev-apps-entrypoint.sh / dev-apps-supergraph.yaml
  仅等待 5 个子图(iam/core-edu/content/msg/config-service)
  避免等待未启动的 ai/data-ana
- router.yaml:移除 v1.45 不兼容键
  (require_manifest/manifest_path/max_cost/max_batch_size)
  移除 ELv2 受限特性(persisted_queries/limits.max_depth)
  自托管无 GraphOS license
- docker-compose.yml:config-service 添加 NODE_ENV=production
  修复 pino-pretty devDep 误用
  apollo-router 添加 APOLLO_PERSISTED_QUERIES env
- services/{classes,core-edu,content,config-service}/Dockerfile
  重写为 repo 根 context + shared-ts/shared-proto 工作区依赖
  + --ignore-scripts
  修复 ERR_PNPM_WORKSPACE_PKG_NOT_FOUND 与 ERR_PNPM_IGNORED_BUILDS
- api-gateway healthcheck:wget --spider 改为 wget -O /dev/null
  修复 busybox 对 200+body 误报 exit 8
This commit is contained in:
SpecialX
2026-07-22 11:35:02 +08:00
parent f586a0b19e
commit 682f323bad
10 changed files with 603 additions and 71 deletions

View File

@@ -0,0 +1,326 @@
# ============================================================
# Docker Compose Override本地开发态应用服务v2.1
#
# 用途:将 8 个应用服务iam/classes/core-edu/content/msg/api-gateway/push-gateway/iam也以容器方式运行
# 与 infra/docker-compose.yml 中的基础设施MySQL/Redis/Kafka/Apollo Router/portal-shell共用 default 网络。
#
# 启动方式:
# docker compose -f infra/docker-compose.yml -f infra/docker-compose.dev-apps.yml --profile p3 up -d
#
# 与 infra/docker-compose.yml 的关系:
# - 同 name: edu-full共用 project 命名空间与 default 网络)
# - 应用服务通过容器名edu-mysql/edu-redis/edu-kafka访问基础设施
# - apollo-router 已在 infra/docker-compose.yml 定义,此处不重复
# - config-service 已在 infra/docker-compose.yml 定义,此处不重复
#
# 注意:本文件只补充 docker-compose.yml 中缺失的 7 个应用服务(不含已定义的 config-service/apollo-router/portal-shell
# ============================================================
name: edu-full
services:
# ============================================================
# IAM 服务NestJS端口 3002 + gRPC 50052
# 依赖MySQL用户/角色/权限表、Redis缓存、KafkaOutbox 事件)
# RS256 密钥:由 infra/keys/iam-{private,public}.pem 挂载(首次启动需生成,见 scripts/gen-iam-keys.ps1
# ============================================================
iam:
build:
context: ..
dockerfile: services/iam/Dockerfile
container_name: edu-iam
restart: unless-stopped
environment:
PORT: "3002"
GRPC_PORT: "50052"
DATABASE_URL: mysql://edu:${MYSQL_PASSWORD:-changeme}@edu-mysql:3306/next_edu_cloud
REDIS_URL: redis://edu-redis:6379
KAFKA_BROKERS: kafka:29092
KAFKA_CLIENT_ID: iam-service
IAM_PRIVATE_KEY_PATH: /keys/iam-private.pem
IAM_PUBLIC_KEY_PATH: /keys/iam-public.pem
JWT_ISSUER: ${JWT_ISSUER:-next-edu-cloud}
JWT_AUDIENCE: ${JWT_AUDIENCE:-next-edu-cloud}
JWT_KEY_ID: iam-rs256-v1
ACCESS_TOKEN_TTL: "15m"
REFRESH_TOKEN_TTL_DAYS: "7"
COOKIE_SECURE: "auto"
OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://edu-jaeger:4318}
LOG_LEVEL: ${LOG_LEVEL:-info}
NODE_ENV: production
# DEV_MODE=true 让子图 middleware 跳过鉴权,允许 apollo-router rover introspect 子图
# 外网请求经 api-gatewayDEV_MODE=falseRS256 验签)入站,子图在 docker 内网可信
DEV_MODE: "true"
ports:
- "3002:3002"
- "50052:50052"
volumes:
- ../infra/keys:/keys:ro
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_started
healthcheck:
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:3002/healthz"]
interval: 15s
timeout: 5s
start_period: 30s
retries: 10
# ============================================================
# Classes 服务NestJS端口 3001
# 依赖MySQL、Redis、Kafka可选
# ============================================================
classes:
build:
context: ..
dockerfile: services/classes/Dockerfile
container_name: edu-classes
restart: unless-stopped
environment:
PORT: "3001"
DATABASE_URL: mysql://edu:${MYSQL_PASSWORD:-changeme}@edu-mysql:3306/next_edu_cloud
REDIS_URL: redis://edu-redis:6379
KAFKA_BROKERS: kafka:29092
JWT_SECRET: ${JWT_SECRET:-p1-dev-secret-change-in-production}
JWT_ISSUER: ${JWT_ISSUER:-next-edu-cloud}
OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://edu-jaeger:4318}
LOG_LEVEL: ${LOG_LEVEL:-info}
NODE_ENV: production
# DEV_MODE=true 让子图 middleware 跳过鉴权,允许 apollo-router rover introspect 子图
DEV_MODE: "true"
ports:
- "3001:3001"
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_started
healthcheck:
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:3001/healthz"]
interval: 15s
timeout: 5s
start_period: 30s
retries: 10
# ============================================================
# Core-Edu 服务NestJS端口 3004 + gRPC 50054
# 依赖MySQL、Redis、KafkaOutbox 事件)
# ============================================================
core-edu:
build:
context: ..
dockerfile: services/core-edu/Dockerfile
container_name: edu-core-edu
restart: unless-stopped
environment:
PORT: "3004"
GRPC_PORT: "50054"
DATABASE_URL: mysql://edu:${MYSQL_PASSWORD:-changeme}@edu-mysql:3306/next_edu_cloud
REDIS_URL: redis://edu-redis:6379
KAFKA_BROKERS: kafka:29092
OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://edu-jaeger:4318}
LOG_LEVEL: ${LOG_LEVEL:-info}
NODE_ENV: production
# DEV_MODE=true 让子图 middleware 跳过鉴权,允许 apollo-router rover introspect 子图
DEV_MODE: "true"
ports:
- "3004:3004"
- "50054:50054"
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_started
healthcheck:
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:3004/healthz"]
interval: 15s
timeout: 5s
start_period: 30s
retries: 10
# ============================================================
# Content 服务NestJS端口 3005 + gRPC 50055
# 依赖MySQL、Redis、KafkaOutbox、Neo4j可选降级、ES可选降级
# ============================================================
content:
build:
context: ..
dockerfile: services/content/Dockerfile
container_name: edu-content
restart: unless-stopped
environment:
PORT: "3005"
GRPC_PORT: "50055"
DATABASE_URL: mysql://edu:${MYSQL_PASSWORD:-changeme}@edu-mysql:3306/next_edu_cloud
REDIS_URL: redis://edu-redis:6379
KAFKA_BROKERS: kafka:29092
NEO4J_URL: bolt://neo4j:7687
NEO4J_USER: neo4j
NEO4J_PASSWORD: ${NEO4J_PASSWORD:-changeme}
ES_URL: http://elasticsearch:9200
OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://edu-jaeger:4318}
LOG_LEVEL: ${LOG_LEVEL:-info}
NODE_ENV: production
# DEV_MODE=true 让子图 middleware 跳过鉴权,允许 apollo-router rover introspect 子图
DEV_MODE: "true"
ports:
- "3005:3005"
- "50055:50055"
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_started
healthcheck:
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:3005/healthz"]
interval: 15s
timeout: 5s
start_period: 30s
retries: 10
# ============================================================
# Msg 服务NestJS端口 3007 + gRPC 50057
# 依赖MySQL、Redis、Kafka、ES可选、push-gatewayHTTP /internal/push
# ============================================================
msg:
build:
context: ..
dockerfile: services/msg/Dockerfile
container_name: edu-msg
restart: unless-stopped
environment:
PORT: "3007"
GRPC_PORT: "50057"
DATABASE_URL: mysql://edu:${MYSQL_PASSWORD:-changeme}@edu-mysql:3306/next_edu_cloud
REDIS_URL: redis://edu-redis:6379
KAFKA_BROKERS: kafka:29092
ES_URL: http://elasticsearch:9200
PUSH_GATEWAY_URL: http://push-gateway:8081
PUSH_INTERNAL_TOKEN: ${PUSH_INTERNAL_TOKEN:-edu-internal-token}
OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://edu-jaeger:4318}
LOG_LEVEL: ${LOG_LEVEL:-info}
NODE_ENV: production
# DEV_MODE=true 让子图 middleware 跳过鉴权,允许 apollo-router rover introspect 子图
DEV_MODE: "true"
ports:
- "3007:3007"
- "50057:50057"
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_started
push-gateway:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:3007/healthz"]
interval: 15s
timeout: 5s
start_period: 30s
retries: 10
# ============================================================
# Push-Gateway 服务Go端口 8081
# 依赖RedisPub/Sub backplane、JWKSiam 公钥验签)
# ============================================================
push-gateway:
build:
context: ..
dockerfile: services/push-gateway/Dockerfile
container_name: edu-push-gateway
restart: unless-stopped
environment:
PUSH_GATEWAY_PORT: "8081"
JWT_SECRET: ${JWT_SECRET:-p1-dev-secret-change-in-production}
REDIS_URL: redis://edu-redis:6379
PUSH_INTERNAL_TOKEN: ${PUSH_INTERNAL_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,http://localhost:4010}
MAX_CONNS_PER_USER: "5"
HEARTBEAT_INTERVAL_SECONDS: "30"
OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://edu-jaeger:4318}
DEV_MODE: "false"
ports:
- "8081:8081"
depends_on:
redis:
condition: service_started
healthcheck:
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://localhost:8081/healthz"]
interval: 15s
timeout: 5s
start_period: 10s
retries: 5
# ============================================================
# API-Gateway 服务Go端口 8080
# 依赖iamJWKS、apollo-router、所有后端服务HTTP 路由代理)
# ============================================================
api-gateway:
build:
context: ..
dockerfile: services/api-gateway/Dockerfile
container_name: edu-api-gateway
restart: unless-stopped
environment:
API_GATEWAY_PORT: "8080"
ENV: production
JWT_SECRET: ${JWT_SECRET:-p1-dev-secret-change-in-production}
JWT_ISSUER: ${JWT_ISSUER:-next-edu-cloud}
JWT_AUDIENCE: ${JWT_AUDIENCE:-next-edu-cloud}
IAM_JWKS_URL: http://iam:3002/v1/iam/.well-known/jwks.json
DEV_MODE: "false"
CLASSES_SERVICE_URL: http://classes:3001
IAM_SERVICE_URL: http://iam:3002
APOLLO_ROUTER_URL: http://apollo-router:3000
CORE_EDU_SERVICE_URL: http://core-edu:3004
CONTENT_SERVICE_URL: http://content:3005
MSG_SERVICE_URL: http://msg:3007
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://edu-jaeger:4318}
LOG_LEVEL: ${LOG_LEVEL:-info}
ports:
- "8080:8080"
depends_on:
iam:
condition: service_healthy
apollo-router:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://localhost:8080/healthz"]
interval: 15s
timeout: 5s
start_period: 10s
retries: 5
# ============================================================
# Apollo Router override覆盖 docker-compose.yml 中的同名服务)
#
# 覆盖原因:默认 entrypoint.sh 会等待 ai/data-ana 子图(用户未启动),
# 默认 supergraph.yaml 包含 ai/data-ana 子图rover compose 会失败)。
#
# 覆盖内容:
# - 挂载 dev-apps-supergraph.yaml仅 iam/core-edu/content/msg/config 5 个子图)
# - 挂载 dev-apps-entrypoint.sh仅等待 5 个子图)
# - 添加 depends_on 5 个子图health check 通过后才启动 router
# ============================================================
apollo-router:
volumes:
# 追加覆盖默认 supergraph.yaml 与 entrypoint.sh
- ./apollo-router/dev-apps-supergraph.yaml:/dist/supergraph.yaml:ro
- ./apollo-router/dev-apps-entrypoint.sh:/dist/entrypoint.sh:ro
# 保留原 pq-manifest 挂载(来自 docker-compose.yml
- ../apps/portal-shell/public/pq-manifest.json:/etc/apollo-router/pq-manifest.json:ro
depends_on:
iam:
condition: service_healthy
config-service:
condition: service_healthy
core-edu:
condition: service_healthy
content:
condition: service_healthy
msg:
condition: service_healthy