From 4a0893ef52195513aaa3d04660ba5d8139570378 Mon Sep 17 00:00:00 2001 From: SpecialX <47072643+wangxiner55@users.noreply.github.com> Date: Wed, 8 Jul 2026 15:12:34 +0800 Subject: [PATCH] =?UTF-8?q?feat(infra):=20=E6=96=B0=E5=A2=9E=E7=94=9F?= =?UTF-8?q?=E4=BA=A7=20Dockerfile=20=E4=B8=8E=E9=83=A8=E7=BD=B2=20compose?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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: 部署环境变量模板 --- apps/teacher-portal/Dockerfile | 49 +++++++++++++++ infra/deploy.env.example | 38 ++++++++++++ infra/docker-compose.deploy.yml | 105 ++++++++++++++++++++++++++++++++ infra/docker-compose.prod.yml | 105 ++++++++++++++++++++++++++++++++ services/api-gateway/Dockerfile | 39 +++++++++--- 5 files changed, 328 insertions(+), 8 deletions(-) create mode 100644 apps/teacher-portal/Dockerfile create mode 100644 infra/deploy.env.example create mode 100644 infra/docker-compose.deploy.yml create mode 100644 infra/docker-compose.prod.yml diff --git a/apps/teacher-portal/Dockerfile b/apps/teacher-portal/Dockerfile new file mode 100644 index 0000000..4fda909 --- /dev/null +++ b/apps/teacher-portal/Dockerfile @@ -0,0 +1,49 @@ +# 多阶段构建:Next.js 生产镜像 +# 用法:docker build -t edu/teacher-portal:latest -f apps/teacher-portal/Dockerfile . + +# ============ Builder ============ +FROM node:20-alpine AS builder +WORKDIR /app + +# 启用 pnpm +RUN corepack enable && corepack prepare pnpm@9.12.0 --activate + +# 先拷依赖清单,利用缓存 +COPY package.json pnpm-lock.yaml* pnpm-workspace.yaml* ./ +COPY apps/teacher-portal/package.json ./apps/teacher-portal/ +# 安装依赖(含 devDependencies,构建需要) +RUN pnpm install --filter @edu/teacher-portal... --frozen-lockfile || pnpm install --filter @edu/teacher-portal... + +# 拷源码 +COPY apps/teacher-portal ./apps/teacher-portal + +# 构建(禁用 telemetry,生产模式) +ENV NEXT_TELEMETRY_DISABLED=1 +RUN pnpm --filter @edu/teacher-portal run build + +# ============ Runtime ============ +FROM node:20-alpine AS runner +WORKDIR /app + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 +ENV PORT=3000 + +# 非 root 用户运行 +RUN addgroup -g 1001 -S nodejs && adduser -S nextjs -u 1001 + +# 拷构建产物与必要清单 +COPY --from=builder /app/apps/teacher-portal/package.json ./package.json +COPY --from=builder /app/apps/teacher-portal/.next ./.next +COPY --from=builder /app/apps/teacher-portal/public ./public +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/apps/teacher-portal/next.config.js ./next.config.js + +USER nextjs +EXPOSE 3000 + +# 健康检查 +HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ + CMD wget --quiet --spider http://localhost:3000/ || exit 1 + +CMD ["node_modules/.bin/next", "start", "-p", "3000"] diff --git a/infra/deploy.env.example b/infra/deploy.env.example new file mode 100644 index 0000000..ff3c056 --- /dev/null +++ b/infra/deploy.env.example @@ -0,0 +1,38 @@ +# 服务器部署环境变量模板 +# 使用方式:复制到 /opt/edu/.env 并填入生产值 +# cp infra/deploy.env.example /opt/edu/.env +# vim /opt/edu/.env +# +# 安全警告: +# - 此文件含敏感信息,禁止提交到 Git +# - .gitignore 已忽略 .env +# - 仅 SRE AI 或人类决策者可编辑 + +# ============ 数据库(服务器已有 MySQL)============ +# DATABASE_URL 中的 host 需用容器名(如 edu-mysql)而非 localhost +# 因为应用容器通过 edu-shared 网络访问 MySQL 容器 +DATABASE_URL=mysql://edu:changeme@edu-mysql:3306/next_edu_cloud + +# ============ Redis(服务器已有 Redis)============ +# 同理用容器名 +REDIS_URL=redis://edu-redis:6379 + +# ============ JWT(生产密钥,必须修改)============ +# 生成方式:openssl rand -hex 32 +JWT_SECRET=CHANGE_ME_TO_STRONG_RANDOM_SECRET +JWT_ISSUER=next-edu-cloud +JWT_AUDIENCE=next-edu-cloud + +# ============ 服务端口 ============ +API_GATEWAY_PORT=8080 +TEACHER_PORTAL_PORT=3000 + +# ============ Kafka(P3 阶段启用,P1 留空)============ +KAFKA_BROKERS= + +# ============ 可观测性(P6 阶段启用,P1 留空)============ +OTEL_EXPORTER_OTLP_ENDPOINT= +LOG_LEVEL=info + +# ============ 镜像 tag(由 CI 注入,手动部署时可改)============ +# IMAGE_TAG=latest ← 默认 latest,CI 通过 export IMAGE_TAG 覆盖 diff --git a/infra/docker-compose.deploy.yml b/infra/docker-compose.deploy.yml new file mode 100644 index 0000000..0a23594 --- /dev/null +++ b/infra/docker-compose.deploy.yml @@ -0,0 +1,105 @@ +# 服务器部署用 Docker Compose +# 镜像来源:Gitea Container Registry (git.eazygame.cn/xiner/edu/:) +# 基础设施: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 -p +# 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 diff --git a/infra/docker-compose.prod.yml b/infra/docker-compose.prod.yml new file mode 100644 index 0000000..5ef8cab --- /dev/null +++ b/infra/docker-compose.prod.yml @@ -0,0 +1,105 @@ +# 生产环境应用服务编排 +# 仅包含应用服务(api-gateway / classes / teacher-portal),基础设施请使用 docker-compose.yml 或 K8s +# +# 使用方式: +# 1. 先构建镜像: +# docker compose -f infra/docker-compose.prod.yml build +# 2. 启动(依赖基础设施已运行): +# docker compose -f infra/docker-compose.prod.yml up -d +# 3. 查看日志: +# docker compose -f infra/docker-compose.prod.yml logs -f +# +# 前置条件: +# - MySQL(:3306)、Redis(:6379)已通过 docker-compose.minimal.yml 或外部方式启动 +# - .env 文件已配置(DEV_MODE 必须为 false) + +name: edu-prod + +services: + api-gateway: + build: + context: .. + dockerfile: services/api-gateway/Dockerfile + image: edu/api-gateway: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 + + classes: + build: + context: .. + dockerfile: services/classes/Dockerfile + image: edu/classes:latest + 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} + # classes 连接宿主机 MySQL/Redis 时,host.docker.internal 在 Docker Desktop 上可用 + extra_hosts: + - "host.docker.internal:host-gateway" + healthcheck: + test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:3001/healthz"] + interval: 30s + timeout: 5s + start_period: 30s + retries: 5 + networks: + - edu-net + + teacher-portal: + build: + context: .. + dockerfile: apps/teacher-portal/Dockerfile + image: edu/teacher-portal: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 + +networks: + edu-net: + driver: bridge diff --git a/services/api-gateway/Dockerfile b/services/api-gateway/Dockerfile index ee0630d..c00b561 100644 --- a/services/api-gateway/Dockerfile +++ b/services/api-gateway/Dockerfile @@ -1,15 +1,38 @@ -# Build stage +# 多阶段构建:api-gateway 生产镜像 +# 用法:docker build -t edu/api-gateway:latest -f services/api-gateway/Dockerfile . + +# ============ Builder ============ FROM golang:1.22-alpine AS builder WORKDIR /app + +# git 与 ca-certificates 为 go mod 下载所需 +RUN apk add --no-cache git ca-certificates + +# 先拷依赖清单利用缓存 COPY go.mod go.sum ./ RUN go mod download -COPY . . -RUN CGO_ENABLED=0 GOOS=linux go build -o api-gateway . -# Runtime stage -FROM alpine:3.20 -RUN apk --no-cache add ca-certificates +# 拷源码并构建 +COPY . . +# 静态编译,CGO_DISABLED 便于 alpine 运行 +RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/bin/api-gateway ./main.go + +# ============ Runtime ============ +FROM alpine:3.20 AS runner WORKDIR /app -COPY --from=builder /app/api-gateway . + +RUN apk add --no-cache ca-certificates tzdata wget + +# 非 root 用户 +RUN addgroup -g 1001 -S app && adduser -S app -u 1001 -G app + +COPY --from=builder /app/bin/api-gateway /app/api-gateway + +USER app EXPOSE 8080 -CMD ["./api-gateway"] + +# 健康检查 +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD wget --quiet --spider http://localhost:8080/healthz || exit 1 + +ENTRYPOINT ["/app/api-gateway"]