# 服务器部署用 Docker Compose(no-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 M9:apollo-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 M3:config-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 M9:teacher-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.yaml(Dockerfile 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 M10:teacher-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