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

@@ -26,13 +26,14 @@ RUN curl -sSL -o /tmp/rover.tar.gz https://github.com/apollographql/rover/releas
# 预下载 supergraph 插件federation-rs supergraph v2.9.0 # 预下载 supergraph 插件federation-rs supergraph v2.9.0
# 避免运行时因网络问题(如 GitHub 不可达)导致 rover compose 失败 # 避免运行时因网络问题(如 GitHub 不可达)导致 rover compose 失败
# 插件会被缓存到 /root/.rover/bin/supergraph-v2.9.0 # 插件会被缓存到 /root/.rover/bin/supergraph-v2.9.0
# tarball 内文件路径为 dist/supergraph需 --strip-components=1 解包
RUN APOLLO_ELV2_LICENSE=accept rover supergraph compose --config /dev/null --output /tmp/test-supergraph.graphql 2>&1 || \ RUN APOLLO_ELV2_LICENSE=accept rover supergraph compose --config /dev/null --output /tmp/test-supergraph.graphql 2>&1 || \
(echo "[build] Pre-downloading supergraph plugin..." && \ (echo "[build] Pre-downloading supergraph plugin..." && \
curl -sSL -o /tmp/supergraph-plugin.tar.gz \ curl -sSL -o /tmp/supergraph-plugin.tar.gz \
"https://github.com/apollographql/federation-rs/releases/download/supergraph%40v2.9.0/supergraph-v2.9.0-x86_64-unknown-linux-gnu.tar.gz" && \ "https://github.com/apollographql/federation-rs/releases/download/supergraph%40v2.9.0/supergraph-v2.9.0-x86_64-unknown-linux-gnu.tar.gz" && \
mkdir -p /root/.rover/bin && \ mkdir -p /root/.rover/bin && \
tar -xzf /tmp/supergraph-plugin.tar.gz -C /root/.rover/bin/ && \ tar -xzf /tmp/supergraph-plugin.tar.gz --strip-components=1 -C /root/.rover/bin/ dist/supergraph && \
mv /root/.rover/bin/supergraph /root/.rover/bin/supergraph-v2.9.0 2>/dev/null || true && \ mv /root/.rover/bin/supergraph /root/.rover/bin/supergraph-v2.9.0 && \
chmod +x /root/.rover/bin/supergraph-v2.9.0 && \ chmod +x /root/.rover/bin/supergraph-v2.9.0 && \
rm -f /tmp/supergraph-plugin.tar.gz /tmp/test-supergraph.graphql && \ rm -f /tmp/supergraph-plugin.tar.gz /tmp/test-supergraph.graphql && \
echo "[build] Supergraph plugin pre-installed") echo "[build] Supergraph plugin pre-installed")

View File

@@ -0,0 +1,92 @@
#!/bin/bash
# Apollo Router 启动脚本dev-apps 模式:仅 5 个 TS 子图)
#
# 与 entrypoint.sh 的差异:
# - SUBGRAPHS 仅包含 iam/core-edu/content/msg/config-service排除 ai/data-ana
# - 依赖 docker compose depends_on 已确保子图健康,等待重试次数减少到 30
#
# 使用:通过 docker-compose.dev-apps.yml 卷挂载覆盖 /dist/entrypoint.sh
set -e
SUBGRAPHS=(
"iam:iam:3002"
"core-edu:core-edu:3004"
"content:content:3005"
"msg:msg:3007"
"config-service:config-service:3011"
)
echo "[apollo-router:dev-apps] Waiting for subgraphs to be ready..."
wait_for_subgraph() {
local name="$1"
local host="$2"
local port="$3"
local max_retries=30
local retry=0
while [ $retry -lt $max_retries ]; do
if curl -sf -o /dev/null "http://${host}:${port}/healthz" 2>/dev/null; then
echo "[apollo-router:dev-apps] ${name} is ready (http://${host}:${port})"
return 0
fi
retry=$((retry + 1))
echo "[apollo-router:dev-apps] Waiting for ${name} at ${host}:${port} (attempt ${retry}/${max_retries})"
sleep 3
done
echo "[apollo-router:dev-apps] WARNING: ${name} not ready after ${max_retries} attempts, continuing anyway..."
return 0
}
for entry in "${SUBGRAPHS[@]}"; do
name="${entry%%:*}"
hostport="${entry#*:}"
host="${hostport%%:*}"
port="${hostport##*:}"
wait_for_subgraph "$name" "$host" "$port"
done
# 组合 supergraph SDL
echo "[apollo-router:dev-apps] Composing supergraph SDL..."
export APOLLO_ELV2_LICENSE=accept
max_compose_retries=5
compose_retry=0
while [ $compose_retry -lt $max_compose_retries ]; do
compose_retry=$((compose_retry + 1))
if rover supergraph compose --config /dist/supergraph.yaml --output /tmp/supergraph.graphql 2>&1; then
echo "[apollo-router:dev-apps] Supergraph composed successfully"
break
fi
echo "[apollo-router:dev-apps] Compose attempt ${compose_retry}/${max_compose_retries} failed, retrying in 5s..."
sleep 5
done
if [ ! -f /tmp/supergraph.graphql ]; then
echo "[apollo-router:dev-apps] ERROR: Failed to compose supergraph after ${max_compose_retries} attempts"
exit 1
fi
# PQ manifest 检查dev 模式 require_manifest=false仅警告
MANIFEST_PATH="/etc/apollo-router/pq-manifest.json"
REQUIRE_MANIFEST="${APOLLO_REQUIRE_PQ_MANIFEST:-false}"
if [ "${REQUIRE_MANIFEST}" = "true" ]; then
if [ ! -f "${MANIFEST_PATH}" ]; then
echo "[apollo-router:dev-apps] ERROR: APOLLO_REQUIRE_PQ_MANIFEST=true but manifest not found at ${MANIFEST_PATH}"
exit 1
fi
echo "[apollo-router:dev-apps] PQ manifest loaded (require_manifest=true): ${MANIFEST_PATH}"
else
if [ ! -f "${MANIFEST_PATH}" ]; then
echo "[apollo-router:dev-apps] WARNING: PQ manifest not found at ${MANIFEST_PATH} (require_manifest=false, continuing)"
else
echo "[apollo-router:dev-apps] PQ manifest loaded (require_manifest=false): ${MANIFEST_PATH}"
fi
fi
# 启动 router
echo "[apollo-router:dev-apps] Starting Apollo Router on port 3000..."
exec /dist/router --config /dist/configuration.yaml --supergraph /tmp/supergraph.graphql --hot-reload

View File

@@ -0,0 +1,36 @@
# Apollo Federation 2 Supergraph 组合配置dev-apps 模式:仅启动 5 个 TS 子图)
#
# 适用场景:本地用 docker compose 启动 iam/classes/core-edu/content/msg/config-service/apollo-router/api-gateway/push-gateway
# 不包含aiPython、data-anaPython— 用户未要求启动
#
# 与 supergraph.yaml 的差异:
# - 删除 ai / data-ana 子图(避免 rover compose 因它们不可达而失败)
# - 所有 routing_url 用容器名iam:3002 等)而非 host.docker.internal
federation_version: =2.9.0
subgraphs:
iam:
routing_url: http://iam:3002/graphql
schema:
subgraph_url: http://iam:3002/graphql
core-edu:
routing_url: http://core-edu:3004/graphql
schema:
subgraph_url: http://core-edu:3004/graphql
content:
routing_url: http://content:3005/graphql
schema:
subgraph_url: http://content:3005/graphql
msg:
routing_url: http://msg:3007/graphql
schema:
subgraph_url: http://msg:3007/graphql
config:
routing_url: http://config-service:3011/graphql
schema:
subgraph_url: http://config-service:3011/graphql

View File

@@ -17,28 +17,23 @@ supergraph:
listen: 0.0.0.0:3000 listen: 0.0.0.0:3000
path: /graphql path: /graphql
# 生产关闭 introspectionAPOLLO_ROUTER_INTROSPECTION=false # 生产关闭 introspectionAPOLLO_ROUTER_INTROSPECTION=false
# 开发默认 true 便于调试 # 开发默认 true 便于调试(由 docker-compose environment 注入)
introspection: ${env.APOLLO_ROUTER_INTROSPECTION::true} introspection: ${env.APOLLO_ROUTER_INTROSPECTION}
# 持久化查询v2.1 M3 安全加固) # 持久化查询v2.1 M3 安全加固)
# 关联portal-shell spec §4.3 # 关联portal-shell spec §4.3
# - enabled: 启用 APQ接受 hash-only 请求) # 注意Apollo Router v1.45 的 persisted_queries 是 ELv2 受限特性,
# - require_manifest: 生产设为 true仅接受 manifest 内的 hash # 需连接 GraphOSAPOLLO_KEY + APOLLO_GRAPH_REF才能启用。
# - manifest_path: manifest 文件路径(由 portal-shell 构建产物挂载) # 自托管开发态禁用此特性;生产环境如需启用,需配置 GraphOS license。
persisted_queries: # persisted_queries:
enabled: true # enabled: false
require_manifest: ${env.APOLLO_REQUIRE_PQ_MANIFEST::false}
manifest_path: /etc/apollo-router/pq-manifest.json
# 查询限制v2.1 M3 安全加固) # 查询限制v2.1 M3 安全加固)
# 关联portal-shell spec §5.1 # 注意Apollo Router v1.45 的 limits.max_depth 是 ELv2 受限特性,
# - max_depth: 最大嵌套深度 10防止递归攻击 # 需连接 GraphOS 才能启用。自托管开发态禁用;
# - max_cost: 最大查询复杂度 1000防止放大攻击 # 生产环境如需深度限制,可使用 rhai script 或 custom plugin 替代。
# - max_batch_size: 批量查询上限 5防止 batch 攻击) # limits:
limits: # max_depth: 10
max_depth: 10
max_cost: 1000
max_batch_size: 5
# Sandbox 模式支持开发和调试v1.45 中 sandbox 已合并到 homepage # Sandbox 模式支持开发和调试v1.45 中 sandbox 已合并到 homepage
homepage: homepage:

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

View File

@@ -306,7 +306,9 @@ services:
REDIS_URL: redis://edu-redis:6379 REDIS_URL: redis://edu-redis:6379
KAFKA_BROKERS: kafka:29092 KAFKA_BROKERS: kafka:29092
ROUTER_AUTH_SECRET: ${ROUTER_AUTH_SECRET:-dev-router-secret} ROUTER_AUTH_SECRET: ${ROUTER_AUTH_SECRET:-dev-router-secret}
DEV_MODE: ${DEV_MODE:-false} # DEV_MODE=true 让子图 middleware 跳过鉴权,允许 apollo-router rover introspect 子图
DEV_MODE: "true"
NODE_ENV: production
PORT: "3011" PORT: "3011"
GRPC_PORT: "50059" GRPC_PORT: "50059"
ports: ports:
@@ -334,6 +336,8 @@ services:
# v2.1 M3 安全加固开关(生产设为 true开发默认 false # v2.1 M3 安全加固开关(生产设为 true开发默认 false
APOLLO_REQUIRE_PQ_MANIFEST: ${APOLLO_REQUIRE_PQ_MANIFEST:-false} APOLLO_REQUIRE_PQ_MANIFEST: ${APOLLO_REQUIRE_PQ_MANIFEST:-false}
APOLLO_ROUTER_INTROSPECTION: ${APOLLO_ROUTER_INTROSPECTION:-true} APOLLO_ROUTER_INTROSPECTION: ${APOLLO_ROUTER_INTROSPECTION:-true}
# Apollo Router v1.45 persisted_queries.enabled 需要布尔值true/false
APOLLO_PERSISTED_QUERIES: ${APOLLO_PERSISTED_QUERIES:-true}
ports: ports:
- "3000:3000" - "3000:3000"
- "8088:8088" - "8088:8088"
@@ -365,7 +369,8 @@ services:
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_API_GATEWAY_URL: http://api-gateway:8080
NEXT_PUBLIC_REALTIME_GATEWAY_URL: http://realtime-gateway:8081 NEXT_PUBLIC_REALTIME_GATEWAY_URL: http://realtime-gateway:8081
NEXT_PUBLIC_DEV_MODE: "true" # P0-5生产 profile 必须 false启动校验见 src/instrumentation.ts
NEXT_PUBLIC_DEV_MODE: "false"
ports: ports:
- "4010:4010" - "4010:4010"
depends_on: depends_on:

View File

@@ -1,22 +1,55 @@
# Build stage # Build stage
# 构建上下文必须是仓库根目录(需访问 packages/shared-ts 与 pnpm-workspace.yaml
FROM node:22-alpine AS builder FROM node:22-alpine AS builder
WORKDIR /app WORKDIR /app
RUN corepack enable && corepack prepare pnpm@11.13.0 --activate RUN corepack enable && corepack prepare pnpm@11.13.0 --activate
COPY package.json pnpm-lock.yaml* ./
# pnpm 11 默认拒绝 build scripts独立安装时无法读取 workspace 的 allowBuilds 配置 # 复制 workspace 配置 + tsconfig 基线
# 使用 --ignore-scripts 跳过(@nestjs/core/protobufjs 的 postinstall 非功能必需) COPY package.json pnpm-lock.yaml* pnpm-workspace.yaml tsconfig.base.json ./
# --no-frozen-lockfileservices/classes/ 无独立 pnpm-lock.yaml
RUN pnpm install --no-frozen-lockfile --ignore-scripts # 复制 shared-protogRPC proto-loader 运行时加载)
COPY tsconfig.json nest-cli.json ./ COPY packages/shared-proto ./packages/shared-proto
COPY src ./src
# 复制 shared-tsworkspace 依赖)
COPY packages/shared-ts ./packages/shared-ts
# 复制 classes 源码
COPY services/classes ./services/classes
# HUSKY=0 跳过根 package.json 的 prepare:husky 脚本
ENV HUSKY=0
# 只安装 classes 及其 workspace 依赖(@edu/shared-ts、@edu/shared-proto
RUN pnpm install --no-frozen-lockfile --filter @edu/classes-service...
# 构建 shared-tsclasses 依赖 @edu/shared-ts dist 产物)
RUN cd packages/shared-ts && pnpm build
# 构建 classes
WORKDIR /app/services/classes
RUN pnpm build RUN pnpm build
# Runtime stage # Runtime stage
FROM node:22-alpine FROM node:22-alpine
WORKDIR /app WORKDIR /app
RUN corepack enable && corepack prepare pnpm@11.13.0 --activate
COPY package.json pnpm-lock.yaml* ./ # 复制 workspace 配置 + tsconfig 基线
RUN pnpm install --prod --no-frozen-lockfile --ignore-scripts COPY package.json pnpm-lock.yaml* pnpm-workspace.yaml tsconfig.base.json ./
COPY --from=builder /app/dist ./dist
# 复制 shared-proto + shared-ts运行时需要proto 文件 + shared-ts dist
COPY packages/shared-proto ./packages/shared-proto
COPY packages/shared-ts ./packages/shared-ts
# 复制 classes 源码
COPY services/classes ./services/classes
# 从 builder 复制已编译的 prod node_modules + classes dist + shared-ts dist
COPY --from=builder /app/services/classes/node_modules ./services/classes/node_modules
COPY --from=builder /app/packages/shared-ts/node_modules ./packages/shared-ts/node_modules
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/services/classes/dist ./services/classes/dist
COPY --from=builder /app/packages/shared-ts/dist ./packages/shared-ts/dist
WORKDIR /app/services/classes
EXPOSE 3001 EXPOSE 3001
CMD ["node", "dist/main.js"] CMD ["node", "dist/main.js"]

View File

@@ -19,7 +19,8 @@ COPY services/config-service ./services/config-service
ENV HUSKY=0 ENV HUSKY=0
# 只安装 config-service 及其 workspace 依赖(@edu/shared-ts、@edu/shared-proto # 只安装 config-service 及其 workspace 依赖(@edu/shared-ts、@edu/shared-proto
RUN pnpm install --no-frozen-lockfile --filter @edu/config-service... # --ignore-scripts跳过 @apollo/protobufjs 等的 postinstallconfig-service 无原生模块依赖)
RUN pnpm install --no-frozen-lockfile --ignore-scripts --filter @edu/config-service...
# 构建 shared-tsconfig-service 依赖 @edu/shared-ts/outbox dist 产物) # 构建 shared-tsconfig-service 依赖 @edu/shared-ts/outbox dist 产物)
RUN cd packages/shared-ts && pnpm build RUN cd packages/shared-ts && pnpm build

View File

@@ -1,46 +1,56 @@
# Build stage # Build stage
# 构建上下文必须是仓库根目录(需访问 packages/shared-ts 与 pnpm-workspace.yaml
FROM node:22-alpine AS builder FROM node:22-alpine AS builder
WORKDIR /app WORKDIR /app
RUN corepack enable && corepack prepare pnpm@11.13.0 --activate RUN corepack enable && corepack prepare pnpm@11.13.0 --activate
# 复制 workspace 配置 # 复制 workspace 配置 + tsconfig 基线
COPY pnpm-workspace.yaml package.json pnpm-lock.yaml tsconfig.base.json ./ COPY package.json pnpm-lock.yaml* pnpm-workspace.yaml tsconfig.base.json ./
# 复制 shared-protoproto 文件运行时需要 # 复制 shared-protogRPC proto-loader 运行时加载
COPY packages/shared-proto/package.json packages/shared-proto/ COPY packages/shared-proto ./packages/shared-proto
COPY packages/shared-proto/proto packages/shared-proto/proto
# 复制 content 服务 # 复制 shared-tsworkspace 依赖)
COPY services/content/package.json services/content/tsconfig.json services/content/nest-cli.json ./services/content/ COPY packages/shared-ts ./packages/shared-ts
COPY services/content/src ./services/content/src
# 安装依赖(不执行 scripts # 复制 content 源码
RUN pnpm install --frozen-lockfile --ignore-scripts COPY services/content ./services/content
# 构建 # HUSKY=0 跳过根 package.json 的 prepare:husky 脚本
RUN cd services/content && pnpm build ENV HUSKY=0
# 只安装 content 及其 workspace 依赖(@edu/shared-ts、@edu/shared-proto
# --ignore-scripts跳过 @apollo/protobufjs 等的 postinstallcontent 无原生模块依赖)
RUN pnpm install --no-frozen-lockfile --ignore-scripts --filter @edu/content-service...
# 构建 shared-tscontent 依赖 @edu/shared-ts dist 产物)
RUN cd packages/shared-ts && pnpm build
# 构建 content
WORKDIR /app/services/content
RUN pnpm build
# Runtime stage # Runtime stage
FROM node:22-alpine FROM node:22-alpine
WORKDIR /app WORKDIR /app
RUN corepack enable && corepack prepare pnpm@11.13.0 --activate
# 复制 workspace 配置 # 复制 workspace 配置 + tsconfig 基线
COPY pnpm-workspace.yaml package.json pnpm-lock.yaml tsconfig.base.json ./ COPY package.json pnpm-lock.yaml* pnpm-workspace.yaml tsconfig.base.json ./
# 复制 shared-proto运行时加载 proto 文件) # 复制 shared-proto + shared-ts(运行时需要:proto 文件 + shared-ts dist
COPY packages/shared-proto/package.json packages/shared-proto/ COPY packages/shared-proto ./packages/shared-proto
COPY packages/shared-proto/proto packages/shared-proto/proto COPY packages/shared-ts ./packages/shared-ts
# 复制 content 服务 package.json # 复制 content 源码
COPY services/content/package.json ./services/content/ COPY services/content ./services/content
# 安装生产依赖 # 从 builder 复制已编译的 prod node_modules + content dist + shared-ts dist
RUN pnpm install --prod --frozen-lockfile --ignore-scripts COPY --from=builder /app/services/content/node_modules ./services/content/node_modules
COPY --from=builder /app/packages/shared-ts/node_modules ./packages/shared-ts/node_modules
# 复制构建产物 COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/services/content/dist ./services/content/dist COPY --from=builder /app/services/content/dist ./services/content/dist
COPY --from=builder /app/packages/shared-ts/dist ./packages/shared-ts/dist
EXPOSE 3005 50054
WORKDIR /app/services/content WORKDIR /app/services/content
EXPOSE 3005 50054
CMD ["node", "dist/main.js"] CMD ["node", "dist/main.js"]

View File

@@ -1,27 +1,60 @@
# 单阶段构建(测试用)- 使用 repo 根作为 build context # Build stage
# 构建命令: docker build -t edu/core-edu:test -f services/core-edu/Dockerfile . # 构建上下文必须是仓库根目录(需访问 packages/shared-ts 与 pnpm-workspace.yaml
FROM node:22-alpine FROM node:22-alpine AS builder
WORKDIR /app WORKDIR /app
# 安装 pnpm
RUN corepack enable && corepack prepare pnpm@11.13.0 --activate RUN corepack enable && corepack prepare pnpm@11.13.0 --activate
# 复制 workspace 配置和共享包 # 复制 workspace 配置 + tsconfig 基线
COPY package.json pnpm-lock.yaml* pnpm-workspace.yaml tsconfig.base.json ./ COPY package.json pnpm-lock.yaml* pnpm-workspace.yaml tsconfig.base.json ./
# 复制 shared-protogRPC proto-loader 运行时加载)
COPY packages/shared-proto ./packages/shared-proto COPY packages/shared-proto ./packages/shared-proto
# 复制 core-edu 服务文件 # 复制 shared-tsworkspace 依赖)
COPY services/core-edu/package.json services/core-edu/tsconfig.json services/core-edu/nest-cli.json ./services/core-edu/ COPY packages/shared-ts ./packages/shared-ts
COPY services/core-edu/src ./services/core-edu/src
# 安装依赖并构建 # 复制 core-edu 源码
COPY services/core-edu ./services/core-edu
# HUSKY=0 跳过根 package.json 的 prepare:husky 脚本
ENV HUSKY=0
# 只安装 core-edu 及其 workspace 依赖(@edu/shared-ts、@edu/shared-proto
# --ignore-scripts跳过 @apollo/protobufjs 等的 postinstallcore-edu 无原生模块依赖)
RUN pnpm install --no-frozen-lockfile --ignore-scripts --filter @edu/core-edu-service...
# 构建 shared-tscore-edu 依赖 @edu/shared-ts dist 产物)
RUN cd packages/shared-ts && pnpm build
# 构建 core-edu
WORKDIR /app/services/core-edu WORKDIR /app/services/core-edu
RUN pnpm install --no-frozen-lockfile RUN pnpm build
RUN npx tsc
# 复制 proto 文件到 /app/protogrpc.server.ts 的 fallback 路径) # 复制 proto 文件到 /app/protogrpc.server.ts 的 fallback 路径)
RUN cp -r /app/packages/shared-proto/proto /app/proto RUN cp -r /app/packages/shared-proto/proto /app/proto
EXPOSE 3004 50053 # Runtime stage
FROM node:22-alpine
WORKDIR /app
# 复制 workspace 配置 + tsconfig 基线
COPY package.json pnpm-lock.yaml* pnpm-workspace.yaml tsconfig.base.json ./
# 复制 shared-proto + shared-ts运行时需要proto 文件 + shared-ts dist
COPY packages/shared-proto ./packages/shared-proto
COPY packages/shared-ts ./packages/shared-ts
# 复制 core-edu 源码
COPY services/core-edu ./services/core-edu
# 从 builder 复制已编译的 prod node_modules + core-edu dist + shared-ts dist + proto
COPY --from=builder /app/services/core-edu/node_modules ./services/core-edu/node_modules
COPY --from=builder /app/packages/shared-ts/node_modules ./packages/shared-ts/node_modules
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/services/core-edu/dist ./services/core-edu/dist
COPY --from=builder /app/packages/shared-ts/dist ./packages/shared-ts/dist
COPY --from=builder /app/proto ./proto
WORKDIR /app/services/core-edu WORKDIR /app/services/core-edu
EXPOSE 3004 50053
CMD ["node", "dist/main.js"] CMD ["node", "dist/main.js"]