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:
49
apps/teacher-portal/Dockerfile
Normal file
49
apps/teacher-portal/Dockerfile
Normal file
@@ -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"]
|
||||||
38
infra/deploy.env.example
Normal file
38
infra/deploy.env.example
Normal file
@@ -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 覆盖
|
||||||
105
infra/docker-compose.deploy.yml
Normal file
105
infra/docker-compose.deploy.yml
Normal 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
|
||||||
105
infra/docker-compose.prod.yml
Normal file
105
infra/docker-compose.prod.yml
Normal file
@@ -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
|
||||||
@@ -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
|
FROM golang:1.22-alpine AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
# git 与 ca-certificates 为 go mod 下载所需
|
||||||
|
RUN apk add --no-cache git ca-certificates
|
||||||
|
|
||||||
|
# 先拷依赖清单利用缓存
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
COPY . .
|
|
||||||
RUN CGO_ENABLED=0 GOOS=linux go build -o api-gateway .
|
|
||||||
|
|
||||||
# Runtime stage
|
# 拷源码并构建
|
||||||
FROM alpine:3.20
|
COPY . .
|
||||||
RUN apk --no-cache add ca-certificates
|
# 静态编译,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
|
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
|
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"]
|
||||||
|
|||||||
Reference in New Issue
Block a user