feat(infra): 新增生产 Dockerfile 与部署 compose

- api-gateway/Dockerfile: 多阶段构建,golang:1.22-alpine → alpine:3.20,CGO_ENABLED=0 静态编译,非 root 运行

- teacher-portal/Dockerfile: 多阶段构建,node:20-alpine builder → runner,含 HEALTHCHECK

- docker-compose.prod.yml: 本地生产编排,3 服务,强制 DEV_MODE=false

- docker-compose.deploy.yml: 服务器部署用,镜像来自 Gitea Registry,通过 edu-shared 外部网络连接 MySQL/Redis

- deploy.env.example: 部署环境变量模板
This commit is contained in:
SpecialX
2026-07-08 15:12:34 +08:00
parent e5902ca2b3
commit 4a0893ef52
5 changed files with 328 additions and 8 deletions

View File

@@ -0,0 +1,105 @@
# 服务器部署用 Docker Compose
# 镜像来源Gitea Container Registry (git.eazygame.cn/xiner/edu/<service>:<tag>)
# 基础设施MySQL + Redis 已在服务器 Docker 中运行(不在此文件管理)
#
# 部署目录:/opt/edu/
# 部署命令CI 自动执行):
# IMAGE_TAG=latest docker compose pull && docker compose up -d
#
# 首次部署手动步骤:
# 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 login git.eazygame.cn -u <user> -p <token>
# 5. cd /opt/edu && IMAGE_TAG=latest docker compose up -d
name: edu
services:
api-gateway:
image: git.eazygame.cn/xiner/edu/api-gateway:${IMAGE_TAG:-latest}
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
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:
image: git.eazygame.cn/xiner/edu/classes:${IMAGE_TAG:-latest}
container_name: edu-classes
restart: unless-stopped
environment:
PORT: 3001
# 连接服务器已有的 MySQL容器名 edu-mysql需在 edu-shared 网络)
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
teacher-portal:
image: git.eazygame.cn/xiner/edu/teacher-portal:${IMAGE_TAG:-latest}
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
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