diff --git a/infra/apollo-router/Dockerfile b/infra/apollo-router/Dockerfile index 06dece0..1cbca6e 100644 --- a/infra/apollo-router/Dockerfile +++ b/infra/apollo-router/Dockerfile @@ -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) # 避免运行时因网络问题(如 GitHub 不可达)导致 rover compose 失败 # 插件会被缓存到 /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 || \ (echo "[build] Pre-downloading supergraph plugin..." && \ 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" && \ mkdir -p /root/.rover/bin && \ - tar -xzf /tmp/supergraph-plugin.tar.gz -C /root/.rover/bin/ && \ - mv /root/.rover/bin/supergraph /root/.rover/bin/supergraph-v2.9.0 2>/dev/null || true && \ + 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 && \ chmod +x /root/.rover/bin/supergraph-v2.9.0 && \ rm -f /tmp/supergraph-plugin.tar.gz /tmp/test-supergraph.graphql && \ echo "[build] Supergraph plugin pre-installed") diff --git a/infra/apollo-router/dev-apps-entrypoint.sh b/infra/apollo-router/dev-apps-entrypoint.sh new file mode 100644 index 0000000..09371d9 --- /dev/null +++ b/infra/apollo-router/dev-apps-entrypoint.sh @@ -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 diff --git a/infra/apollo-router/dev-apps-supergraph.yaml b/infra/apollo-router/dev-apps-supergraph.yaml new file mode 100644 index 0000000..398e177 --- /dev/null +++ b/infra/apollo-router/dev-apps-supergraph.yaml @@ -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 +# 不包含:ai(Python)、data-ana(Python)— 用户未要求启动 +# +# 与 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 diff --git a/infra/apollo-router/router.yaml b/infra/apollo-router/router.yaml index 0f843f4..4560071 100644 --- a/infra/apollo-router/router.yaml +++ b/infra/apollo-router/router.yaml @@ -17,28 +17,23 @@ supergraph: listen: 0.0.0.0:3000 path: /graphql # 生产关闭 introspection(APOLLO_ROUTER_INTROSPECTION=false) - # 开发默认 true 便于调试 - introspection: ${env.APOLLO_ROUTER_INTROSPECTION::true} + # 开发默认 true 便于调试(由 docker-compose environment 注入) + introspection: ${env.APOLLO_ROUTER_INTROSPECTION} # 持久化查询(v2.1 M3 安全加固) # 关联:portal-shell spec §4.3 -# - enabled: 启用 APQ(接受 hash-only 请求) -# - require_manifest: 生产设为 true,仅接受 manifest 内的 hash -# - manifest_path: manifest 文件路径(由 portal-shell 构建产物挂载) -persisted_queries: - enabled: true - require_manifest: ${env.APOLLO_REQUIRE_PQ_MANIFEST::false} - manifest_path: /etc/apollo-router/pq-manifest.json +# 注意:Apollo Router v1.45 的 persisted_queries 是 ELv2 受限特性, +# 需连接 GraphOS(APOLLO_KEY + APOLLO_GRAPH_REF)才能启用。 +# 自托管开发态禁用此特性;生产环境如需启用,需配置 GraphOS license。 +# persisted_queries: +# enabled: false # 查询限制(v2.1 M3 安全加固) -# 关联:portal-shell spec §5.1 -# - max_depth: 最大嵌套深度 10(防止递归攻击) -# - max_cost: 最大查询复杂度 1000(防止放大攻击) -# - max_batch_size: 批量查询上限 5(防止 batch 攻击) -limits: - max_depth: 10 - max_cost: 1000 - max_batch_size: 5 +# 注意:Apollo Router v1.45 的 limits.max_depth 是 ELv2 受限特性, +# 需连接 GraphOS 才能启用。自托管开发态禁用; +# 生产环境如需深度限制,可使用 rhai script 或 custom plugin 替代。 +# limits: +# max_depth: 10 # Sandbox 模式:支持开发和调试(v1.45 中 sandbox 已合并到 homepage) homepage: diff --git a/infra/docker-compose.dev-apps.yml b/infra/docker-compose.dev-apps.yml new file mode 100644 index 0000000..b961751 --- /dev/null +++ b/infra/docker-compose.dev-apps.yml @@ -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(缓存)、Kafka(Outbox 事件) + # 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-gateway(DEV_MODE=false,RS256 验签)入站,子图在 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、Kafka(Outbox 事件) + # ============================================================ + 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、Kafka(Outbox)、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-gateway(HTTP /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) + # 依赖:Redis(Pub/Sub backplane)、JWKS(iam 公钥验签) + # ============================================================ + 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) + # 依赖:iam(JWKS)、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 diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml index 494a43d..566d257 100644 --- a/infra/docker-compose.yml +++ b/infra/docker-compose.yml @@ -306,7 +306,9 @@ services: REDIS_URL: redis://edu-redis:6379 KAFKA_BROKERS: kafka:29092 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" GRPC_PORT: "50059" ports: @@ -334,6 +336,8 @@ services: # v2.1 M3 安全加固开关(生产设为 true,开发默认 false) APOLLO_REQUIRE_PQ_MANIFEST: ${APOLLO_REQUIRE_PQ_MANIFEST:-false} APOLLO_ROUTER_INTROSPECTION: ${APOLLO_ROUTER_INTROSPECTION:-true} + # Apollo Router v1.45 persisted_queries.enabled 需要布尔值(true/false) + APOLLO_PERSISTED_QUERIES: ${APOLLO_PERSISTED_QUERIES:-true} ports: - "3000:3000" - "8088:8088" @@ -365,7 +369,8 @@ services: 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" + # P0-5:生产 profile 必须 false(启动校验见 src/instrumentation.ts) + NEXT_PUBLIC_DEV_MODE: "false" ports: - "4010:4010" depends_on: diff --git a/services/classes/Dockerfile b/services/classes/Dockerfile index 9a016ec..2eb3796 100644 --- a/services/classes/Dockerfile +++ b/services/classes/Dockerfile @@ -1,22 +1,55 @@ # Build stage +# 构建上下文必须是仓库根目录(需访问 packages/shared-ts 与 pnpm-workspace.yaml) FROM node:22-alpine AS builder WORKDIR /app RUN corepack enable && corepack prepare pnpm@11.13.0 --activate -COPY package.json pnpm-lock.yaml* ./ -# pnpm 11 默认拒绝 build scripts,独立安装时无法读取 workspace 的 allowBuilds 配置 -# 使用 --ignore-scripts 跳过(@nestjs/core/protobufjs 的 postinstall 非功能必需) -# --no-frozen-lockfile:services/classes/ 无独立 pnpm-lock.yaml -RUN pnpm install --no-frozen-lockfile --ignore-scripts -COPY tsconfig.json nest-cli.json ./ -COPY src ./src + +# 复制 workspace 配置 + tsconfig 基线 +COPY package.json pnpm-lock.yaml* pnpm-workspace.yaml tsconfig.base.json ./ + +# 复制 shared-proto(gRPC proto-loader 运行时加载) +COPY packages/shared-proto ./packages/shared-proto + +# 复制 shared-ts(workspace 依赖) +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-ts(classes 依赖 @edu/shared-ts dist 产物) +RUN cd packages/shared-ts && pnpm build + +# 构建 classes +WORKDIR /app/services/classes RUN pnpm build # Runtime stage FROM node:22-alpine WORKDIR /app -RUN corepack enable && corepack prepare pnpm@11.13.0 --activate -COPY package.json pnpm-lock.yaml* ./ -RUN pnpm install --prod --no-frozen-lockfile --ignore-scripts -COPY --from=builder /app/dist ./dist + +# 复制 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 + +# 复制 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 CMD ["node", "dist/main.js"] diff --git a/services/config-service/Dockerfile b/services/config-service/Dockerfile index 0b5390b..bec7790 100644 --- a/services/config-service/Dockerfile +++ b/services/config-service/Dockerfile @@ -19,7 +19,8 @@ COPY services/config-service ./services/config-service ENV HUSKY=0 # 只安装 config-service 及其 workspace 依赖(@edu/shared-ts、@edu/shared-proto) -RUN pnpm install --no-frozen-lockfile --filter @edu/config-service... +# --ignore-scripts:跳过 @apollo/protobufjs 等的 postinstall(config-service 无原生模块依赖) +RUN pnpm install --no-frozen-lockfile --ignore-scripts --filter @edu/config-service... # 构建 shared-ts(config-service 依赖 @edu/shared-ts/outbox dist 产物) RUN cd packages/shared-ts && pnpm build diff --git a/services/content/Dockerfile b/services/content/Dockerfile index 60bd7e4..22d9e64 100644 --- a/services/content/Dockerfile +++ b/services/content/Dockerfile @@ -1,46 +1,56 @@ # Build stage +# 构建上下文必须是仓库根目录(需访问 packages/shared-ts 与 pnpm-workspace.yaml) FROM node:22-alpine AS builder WORKDIR /app RUN corepack enable && corepack prepare pnpm@11.13.0 --activate -# 复制 workspace 根配置 -COPY pnpm-workspace.yaml package.json pnpm-lock.yaml tsconfig.base.json ./ +# 复制 workspace 配置 + tsconfig 基线 +COPY package.json pnpm-lock.yaml* pnpm-workspace.yaml tsconfig.base.json ./ -# 复制 shared-proto(proto 文件运行时需要) -COPY packages/shared-proto/package.json packages/shared-proto/ -COPY packages/shared-proto/proto packages/shared-proto/proto +# 复制 shared-proto(gRPC proto-loader 运行时加载) +COPY packages/shared-proto ./packages/shared-proto -# 复制 content 服务 -COPY services/content/package.json services/content/tsconfig.json services/content/nest-cli.json ./services/content/ -COPY services/content/src ./services/content/src +# 复制 shared-ts(workspace 依赖) +COPY packages/shared-ts ./packages/shared-ts -# 安装依赖(不执行 scripts) -RUN pnpm install --frozen-lockfile --ignore-scripts +# 复制 content 源码 +COPY services/content ./services/content -# 构建 -RUN cd services/content && pnpm build +# HUSKY=0 跳过根 package.json 的 prepare:husky 脚本 +ENV HUSKY=0 + +# 只安装 content 及其 workspace 依赖(@edu/shared-ts、@edu/shared-proto) +# --ignore-scripts:跳过 @apollo/protobufjs 等的 postinstall(content 无原生模块依赖) +RUN pnpm install --no-frozen-lockfile --ignore-scripts --filter @edu/content-service... + +# 构建 shared-ts(content 依赖 @edu/shared-ts dist 产物) +RUN cd packages/shared-ts && pnpm build + +# 构建 content +WORKDIR /app/services/content +RUN pnpm build # Runtime stage FROM node:22-alpine WORKDIR /app -RUN corepack enable && corepack prepare pnpm@11.13.0 --activate -# 复制 workspace 配置 -COPY pnpm-workspace.yaml package.json pnpm-lock.yaml tsconfig.base.json ./ +# 复制 workspace 配置 + tsconfig 基线 +COPY package.json pnpm-lock.yaml* pnpm-workspace.yaml tsconfig.base.json ./ -# 复制 shared-proto(运行时加载 proto 文件) -COPY packages/shared-proto/package.json packages/shared-proto/ -COPY packages/shared-proto/proto packages/shared-proto/proto +# 复制 shared-proto + shared-ts(运行时需要:proto 文件 + shared-ts dist) +COPY packages/shared-proto ./packages/shared-proto +COPY packages/shared-ts ./packages/shared-ts -# 复制 content 服务 package.json -COPY services/content/package.json ./services/content/ +# 复制 content 源码 +COPY services/content ./services/content -# 安装生产依赖 -RUN pnpm install --prod --frozen-lockfile --ignore-scripts - -# 复制构建产物 +# 从 builder 复制已编译的 prod node_modules + content dist + shared-ts dist +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/packages/shared-ts/dist ./packages/shared-ts/dist -EXPOSE 3005 50054 WORKDIR /app/services/content +EXPOSE 3005 50054 CMD ["node", "dist/main.js"] diff --git a/services/core-edu/Dockerfile b/services/core-edu/Dockerfile index e530604..6ff9834 100644 --- a/services/core-edu/Dockerfile +++ b/services/core-edu/Dockerfile @@ -1,27 +1,60 @@ -# 单阶段构建(测试用)- 使用 repo 根作为 build context -# 构建命令: docker build -t edu/core-edu:test -f services/core-edu/Dockerfile . -FROM node:22-alpine +# Build stage +# 构建上下文必须是仓库根目录(需访问 packages/shared-ts 与 pnpm-workspace.yaml) +FROM node:22-alpine AS builder WORKDIR /app - -# 安装 pnpm 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 ./ + +# 复制 shared-proto(gRPC proto-loader 运行时加载) COPY packages/shared-proto ./packages/shared-proto -# 复制 core-edu 服务文件 -COPY services/core-edu/package.json services/core-edu/tsconfig.json services/core-edu/nest-cli.json ./services/core-edu/ -COPY services/core-edu/src ./services/core-edu/src +# 复制 shared-ts(workspace 依赖) +COPY packages/shared-ts ./packages/shared-ts -# 安装依赖并构建 +# 复制 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 等的 postinstall(core-edu 无原生模块依赖) +RUN pnpm install --no-frozen-lockfile --ignore-scripts --filter @edu/core-edu-service... + +# 构建 shared-ts(core-edu 依赖 @edu/shared-ts dist 产物) +RUN cd packages/shared-ts && pnpm build + +# 构建 core-edu WORKDIR /app/services/core-edu -RUN pnpm install --no-frozen-lockfile -RUN npx tsc +RUN pnpm build # 复制 proto 文件到 /app/proto(grpc.server.ts 的 fallback 路径) 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 +EXPOSE 3004 50053 CMD ["node", "dist/main.js"]