- Dockerfile 修复:GOPROXY 国内镜像 + go.mod 路径修正 + 构建目录
- docker-compose.deploy.yml 补全 push-gateway 全部 12 个环境变量
- deploy.env.example 新增 push-gateway 段(INTERNAL_API_TOKEN/WS_ALLOWED_ORIGINS)
- 本地 Docker 验证通过(非 mock):
- /healthz + /readyz(Redis + Kafka 健康)
- WebSocket /ws + dev-token 鉴权
- /internal/push HTTP API 投递 3 类考试事件
- Kafka → push-gateway → WebSocket 全链路透传
ExamExtended / ExamForceSubmitted / ExamQuestionReordered
- 新增 docs/nextstep.md:上游依赖(iam/Redis/Kafka)+
下游依赖(4 portal + msg)+ 事件投递架构图 + 环境变量清单
374 lines
11 KiB
YAML
374 lines
11 KiB
YAML
# 服务器部署用 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/services/api-gateway
|
||
dockerfile: Dockerfile
|
||
container_name: edu-api-gateway
|
||
restart: unless-stopped
|
||
environment:
|
||
API_GATEWAY_PORT: ${API_GATEWAY_PORT:-8080}
|
||
JWT_SECRET: ${JWT_SECRET}
|
||
JWT_ISSUER: ${JWT_ISSUER:-next-edu-cloud}
|
||
JWT_AUDIENCE: ${JWT_AUDIENCE:-next-edu-cloud}
|
||
# 生产环境强制关闭 dev-token 旁路
|
||
DEV_MODE: "false"
|
||
CLASSES_SERVICE_URL: http://classes:3001
|
||
IAM_SERVICE_URL: http://iam:3002
|
||
TEACHER_BFF_URL: http://teacher-bff:3003
|
||
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
|
||
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
|
||
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
|
||
|
||
teacher-bff:
|
||
build:
|
||
context: ./repo
|
||
dockerfile: services/teacher-bff/Dockerfile
|
||
container_name: edu-teacher-bff
|
||
restart: unless-stopped
|
||
environment:
|
||
PORT: 3003
|
||
IAM_SERVICE_URL: http://iam:3002
|
||
CLASSES_SERVICE_URL: http://classes:3001
|
||
CORE_EDU_SERVICE_URL: http://core-edu:3004
|
||
LOG_LEVEL: ${LOG_LEVEL:-info}
|
||
NODE_ENV: production
|
||
depends_on:
|
||
iam:
|
||
condition: service_healthy
|
||
classes:
|
||
condition: service_healthy
|
||
healthcheck:
|
||
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:3003/healthz"]
|
||
interval: 30s
|
||
timeout: 5s
|
||
start_period: 20s
|
||
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}
|
||
REDIS_URL: ${REDIS_URL}
|
||
KAFKA_BROKERS: ${KAFKA_BROKERS:-kafka:29092}
|
||
KAFKA_NOTIFICATION_TOPIC: edu.notification.requested
|
||
KAFKA_CONSUMER_GROUP: push-gateway
|
||
INTERNAL_API_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
|
||
|
||
teacher-portal:
|
||
build:
|
||
context: ./repo
|
||
dockerfile: apps/teacher-portal/Dockerfile
|
||
container_name: edu-teacher-portal
|
||
restart: unless-stopped
|
||
environment:
|
||
NODE_ENV: production
|
||
PORT: 3000
|
||
API_GATEWAY_URL: http://api-gateway:8080
|
||
ports:
|
||
- "${TEACHER_PORTAL_PORT:-3000}:3000"
|
||
depends_on:
|
||
api-gateway:
|
||
condition: service_healthy
|
||
healthcheck:
|
||
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:3000/"]
|
||
interval: 30s
|
||
timeout: 5s
|
||
start_period: 20s
|
||
retries: 3
|
||
networks:
|
||
- edu-net
|
||
- edu-shared
|
||
|
||
admin-portal:
|
||
build:
|
||
context: ./repo
|
||
dockerfile: apps/admin-portal/Dockerfile
|
||
container_name: edu-admin-portal
|
||
restart: unless-stopped
|
||
environment:
|
||
NODE_ENV: production
|
||
PORT: 4003
|
||
API_GATEWAY_URL: http://api-gateway:8080
|
||
# 生产环境强制关闭 mock,使用真实后端
|
||
NEXT_PUBLIC_API_MOCKING: "disabled"
|
||
NEXT_PUBLIC_MF_ENABLED: "false"
|
||
NEXT_PUBLIC_SHELL_URL: http://admin-portal:4003
|
||
NEXT_PUBLIC_WS_URL: ws://push-gateway:8081/ws
|
||
NEXT_PUBLIC_OTEL_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://otel-collector:4318}
|
||
NEXT_PUBLIC_WEB_VITALS_ENDPOINT: /api/admin/web-vitals
|
||
ports:
|
||
- "${ADMIN_PORTAL_PORT:-4003}:4003"
|
||
depends_on:
|
||
api-gateway:
|
||
condition: service_healthy
|
||
teacher-bff:
|
||
condition: service_healthy
|
||
healthcheck:
|
||
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:4003/api/ready"]
|
||
interval: 30s
|
||
timeout: 5s
|
||
start_period: 20s
|
||
retries: 3
|
||
networks:
|
||
- edu-net
|
||
- edu-shared
|
||
|
||
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
|