diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 62e2c93..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "permissions": { - "allow": [ - "mcp__plugin_ecc_context7__resolve-library-id", - "mcp__plugin_ecc_context7__query-docs", - "Bash(bun install:*)", - "Bash(bun add:*)", - "Bash(bunx tsc:*)", - "Bash(git status:*)", - "Bash(git add:*)", - "Bash(git commit:*)", - "Bash(bun run:*)" - ] - } -} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..be4af3b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,72 @@ +# Dependencies +node_modules +npm-debug.log +yarn-error.log +bun.lockb + +# Build output +dist +build + +# Environment +.env +.env.* +!.env.example + +# IDE +.vscode +.idea +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Git +.git +.gitignore +.gitattributes + +# CI/CD +.gitea +.github +.gitlab-ci.yml + +# Scripts +scripts + +# Docker +docker-compose*.yml +Dockerfile + +# Docs +docs +*.md +!README.md + +# Tests +coverage +.nyc_output +*.test.ts +*.test.ts +*.spec.ts +__tests__ + +# Tools +.eslintrc +.prettierrc +.editorconfig + +# Drizzle +drizzle + +# Logs +logs +*.log + +# Temporary +tmp +temp +.tmp diff --git a/.env.prod.example b/.env.prod.example new file mode 100644 index 0000000..c2fd3c4 --- /dev/null +++ b/.env.prod.example @@ -0,0 +1,32 @@ +# 生产环境变量模板 +# 复制为 .env.prod 使用 + +# Database +DATABASE_URL=mysql://duoqi_prod:prod-password@your-rds-endpoint:3306/duoqi_prod + +# JWT +JWT_SECRET=prod-super-secret-jwt-key-change-this +JWT_EXPIRES_IN=1h +JWT_REFRESH_EXPIRES_IN=30d + +# Admin +ADMIN_TOKEN=prod-admin-token-change-this + +# Huawei ID Kit +HUAWEI_CLIENT_ID= +HUAWEI_CLIENT_SECRET= + +# Alibaba Cloud OSS +OSS_ACCESS_KEY_ID= +OSS_ACCESS_KEY_SECRET= +OSS_BUCKET=duoqi-assets +OSS_REGION=oss-cn-hangzhou + +# Huawei IAP +HUAWEI_IAP_URL=https://subscr-drcn.iap.hicloud.com +HUAWEI_MERCHANT_ID= + +# Application +PORT=3000 +NODE_ENV=production +LOG_LEVEL=warn diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..7abd1b3 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,198 @@ +# Gitea Actions CI/CD 配置 +# Duoqi API - 双分支工作流(develop → main) +# +# 工作流: +# develop push → quality → test → build → 自动部署测试环境 +# main push → quality → test → build → 手动确认部署生产环境 + +name: CI/CD Pipeline + +on: + push: + branches: [main, develop] + +env: + DEPLOY_DIR: /opt/duoqi-api + +jobs: + # ==================== 代码质量检查 ==================== + quality: + name: Code Quality + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Run ESLint + run: bun run lint + + - name: Type check + run: bun run typecheck + + # ==================== 运行测试 ==================== + test: + name: Unit Tests + runs-on: ubuntu-latest + needs: quality + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Run tests with coverage + run: bun run test:coverage + + - name: Upload coverage + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: coverage/ + + # ==================== 构建测试镜像 ==================== + build-test: + name: Build Test Image + runs-on: ubuntu-latest + needs: [quality, test] + if: github.ref == 'refs/heads/develop' + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Build test image + run: | + docker build --build-arg NODE_ENV=test -t duoqi-api:test . + mkdir -p /tmp/images + docker save duoqi-api:test -o /tmp/images/duoqi-api-test.tar + + - name: Upload image artifact + uses: actions/upload-artifact@v4 + with: + name: docker-images-test + path: /tmp/images/ + retention-days: 1 + + # ==================== 构建生产镜像 ==================== + build-prod: + name: Build Production Image + runs-on: ubuntu-latest + needs: [quality, test] + if: github.ref == 'refs/heads/main' + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Build production image + run: | + docker build -t duoqi-api:prod . + mkdir -p /tmp/images + docker save duoqi-api:prod -o /tmp/images/duoqi-api-prod.tar + + - name: Upload image artifact + uses: actions/upload-artifact@v4 + with: + name: docker-images-prod + path: /tmp/images/ + retention-days: 1 + + # ==================== 部署到测试环境(develop push 自动触发) ==================== + deploy-test: + name: Deploy to Test + runs-on: ubuntu-latest + needs: build-test + if: github.ref == 'refs/heads/develop' + environment: + name: test + url: http://test-api.duoqi.me + steps: + - name: Download image artifact + uses: actions/download-artifact@v4 + with: + name: docker-images-test + path: /tmp/images/ + + - name: Load Docker image + run: docker load -i /tmp/images/duoqi-api-test.tar + + - name: Deploy test environment + run: | + cd ${{ env.DEPLOY_DIR }} + docker-compose --profile test up -d --no-build api-test + + - name: Health check + run: | + sleep 10 + for i in {1..5}; do + if curl -f http://localhost:3001/health; then + echo "Test environment is healthy!" + exit 0 + fi + echo "Health check attempt $i failed, retrying..." + sleep 5 + done + echo "Test environment health check failed" + exit 1 + + # ==================== 部署到生产环境(main push,手动确认) ==================== + deploy-prod: + name: Deploy to Production + runs-on: ubuntu-latest + needs: build-prod + if: github.ref == 'refs/heads/main' + environment: + name: production + url: https://api.duoqi.me + steps: + - name: Download image artifact + uses: actions/download-artifact@v4 + with: + name: docker-images-prod + path: /tmp/images/ + + - name: Load Docker image + run: docker load -i /tmp/images/duoqi-api-prod.tar + + - name: Deploy production + run: | + cd ${{ env.DEPLOY_DIR }} + + # 备份当前镜像(用于回滚) + docker tag duoqi-api:prod duoqi-api:rollback 2>/dev/null || true + + # 滚动更新 + docker-compose up -d --no-build api-prod + + - name: Health check + run: | + sleep 15 + for i in {1..5}; do + if curl -f http://localhost:3000/health; then + echo "Production deployment successful!" + exit 0 + fi + echo "Health check attempt $i failed, retrying..." + sleep 5 + done + + # 健康检查失败,自动回滚 + echo "Health check failed! Rolling back..." + cd ${{ env.DEPLOY_DIR }} + docker tag duoqi-api:rollback duoqi-api:prod + docker-compose up -d --no-build api-prod + exit 1 + + - name: Cleanup + if: always() + run: | + docker image prune -f + rm -rf /tmp/images diff --git a/.gitignore b/.gitignore index 5cdda6f..ebf006e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ dist/ .env *.log db/migrations/ + +# Claude Code +.claude/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..375dc7e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,48 @@ +# Duoqi API - Multi-stage Dockerfile +# 阶段 1: 构建依赖 +FROM oven/bun:1.1 AS base +WORKDIR /app + +# 复制依赖文件 +COPY package.json bun.lockb ./ + +# 阶段 2: 安装依赖 +FROM base AS install +RUN bun install --frozen-lockfile --production + +# 阶段 3: 构建应用 +FROM base AS build +COPY --from=install /app/node_modules ./node_modules +COPY . . + +# 类型检查和编译 +RUN bun run typecheck +RUN bun run build + +# 阶段 4: 生产镜像 +FROM oven/bun:1.1 AS release +WORKDIR /app + +# 创建非 root 用户 +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 bun + +# 复制生产依赖和构建产物 +COPY --from=install /app/node_modules ./node_modules +COPY --from=build /app/dist ./dist +COPY --from=build /app/db ./db +COPY --from=build /app/package.json ./ + +# 设置权限 +RUN chown -R bun:nodejs /app +USER bun + +# 暴露端口 +EXPOSE 3000 + +# 健康检查 +HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ + CMD curl -f http://localhost:3000/health || exit 1 + +# 启动应用 +CMD ["node", "dist/index.js"] diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..491b293 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,68 @@ +# ======================================== +# 服务器部署配置(单服务器 Phase 1) +# 包含生产环境和测试环境(按需启停) +# +# 用法: +# 生产环境:docker-compose up -d +# 含测试: docker-compose --profile test up -d +# 停止测试:docker-compose --profile test stop api-test +# ======================================== + +version: '3.8' + +services: + # ===== 生产环境 ===== + api-prod: + build: + context: . + dockerfile: Dockerfile + image: duoqi-api:prod + container_name: duoqi-api-prod + restart: unless-stopped + env_file: .env.prod + ports: + - "3000:3000" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + deploy: + resources: + limits: + memory: 400M + + # ===== 测试环境(Docker profiles 按需启停) ===== + api-test: + build: + context: . + dockerfile: Dockerfile + image: duoqi-api:test + container_name: duoqi-api-test + restart: "no" + env_file: .env.test + ports: + - "3001:3001" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3001/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + logging: + driver: "json-file" + options: + max-size: "5m" + max-file: "2" + deploy: + resources: + limits: + memory: 300M + profiles: + - test diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..434461f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,71 @@ +# 本地开发环境 Docker Compose 配置 + +version: '3.8' + +services: + # ===== API 服务(热重载开发) ===== + api: + build: + context: . + dockerfile: Dockerfile + container_name: duoqi-api-dev + restart: unless-stopped + env_file: + - .env + ports: + - "3000:3000" + volumes: + - ./src:/app/src:ro + - ./db:/app/db:ro + networks: + - duoqi-network + depends_on: + mysql: + condition: service_healthy + + # ===== MySQL 数据库(本地开发用) ===== + mysql: + image: mysql:8.0 + container_name: duoqi-mysql-dev + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: rootpassword + MYSQL_DATABASE: duoqi_dev + MYSQL_USER: duoqi + MYSQL_PASSWORD: duoqi + ports: + - "3306:3306" + volumes: + - mysql-data:/var/lib/mysql + networks: + - duoqi-network + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-prootpassword"] + interval: 10s + timeout: 5s + retries: 5 + + # ===== Drizzle Studio ===== + studio: + image: node:20-alpine + container_name: duoqi-studio-dev + restart: unless-stopped + working_dir: /app + command: npx drizzle-kit studio + environment: + DATABASE_URL: mysql://duoqi:duoqi@mysql:3306/duoqi_dev + ports: + - "4983:4983" + networks: + - duoqi-network + depends_on: + mysql: + condition: service_healthy + +volumes: + mysql-data: + driver: local + +networks: + duoqi-network: + driver: bridge diff --git a/docs/ci-deployment-guide.md b/docs/ci-deployment-guide.md new file mode 100644 index 0000000..733dcb5 --- /dev/null +++ b/docs/ci-deployment-guide.md @@ -0,0 +1,987 @@ +# Duoqi API 部署与持续集成方案 + +> 基于 Gitea + 阿里云轻量应用服务器 (Alibaba Cloud Linux 3) 的私有化部署方案 + +## 目录 + +- [架构概览](#架构概览) +- [Phase 1: 单服务器方案(当前)](#phase-1-单服务器方案当前) + - [资源规划](#资源规划) + - [服务器初始化](#服务器初始化) + - [Gitea 安装与配置](#gitea-安装与配置) + - [环境隔离策略](#环境隔离策略) + - [CI/CD 流程](#cicd-流程) + - [部署操作](#部署操作) + - [Nginx 配置](#nginx-配置) +- [Phase 2: 多服务器扩展(未来)](#phase-2-多服务器扩展未来) + - [架构演进](#架构演进) + - [私有镜像仓库](#私有镜像仓库) + - [负载均衡与水平扩展](#负载均衡与水平扩展) +- [运维管理](#运维管理) +- [故障排查](#故障排查) +- [附录](#附录) + +--- + +## 架构概览 + +### 为什么选择 Gitea 而不是 GitLab + +| 对比项 | Gitea | GitLab CE | +|--------|-------|-----------| +| 最低内存 | ~200MB | ~4GB | +| 安装复杂度 | 单二进制文件 | 需要多组件 | +| CI/CD | Gitea Actions(兼容 GitHub Actions 语法) | GitLab CI/CD | +| 容器镜像仓库 | 内置(可选) | 内置 | +| 适合场景 | 小团队、资源有限 | 大团队、功能全面 | +| **2C/2G 可行性** | **✅ 完全可行** | **❌ 内存不足** | + +### 单服务器资源分配(2C/2G) + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ 阿里云轻量应用服务器 (2C/2G) │ +│ │ +│ ┌───────────────────────────────────────────────────────────────┐ │ +│ │ Nginx 反向代理 (:80/:443 ← 唯一对外入口) │ │ +│ │ ┌──────────────┐ ┌──────────────┐ ┌────────────────────────┐│ │ +│ │ │ api.duoqi.me│ │test-api. │ │ git.duoqi.me ││ │ +│ │ │ → :3000 prod │ │duoqi.me │ │ → :3200 Gitea (IP限制) ││ │ +│ │ │ │ │ → :3001 test │ │ ││ │ +│ │ └──────────────┘ └──────────────┘ └────────────────────────┘│ │ +│ └───────────────────────────────────────────────────────────────┘ │ +│ │ │ │ │ +│ ▼ ▼ ▼ │ +│ ┌─────────────┐ ┌──────────────┐ ┌──────────────────────────┐ │ +│ │ duoqi-api │ │ duoqi-api │ │ Gitea + Act Runner │ │ +│ │ (prod) │ │ (test) │ │ 代码托管 + CI/CD │ │ +│ │ 127.0.0.1 │ │ 127.0.0.1 │ │ 127.0.0.1:3200 │ │ +│ │ :3000 │ │ :3001 │ │ ~200MB │ │ +│ │ ~300MB │ │ ~200MB │ └──────────────────────────┘ │ +│ └─────────────┘ └──────────────┘ │ +│ │ +│ ┌───────────────────────────────────────────────────────────────┐ │ +│ │ 阿里云 RDS MySQL (外置,仅内网可达) │ │ +│ │ ┌──────────────┐ ┌──────────────┐ │ │ +│ │ │ duoqi_prod │ │ duoqi_test │ │ │ +│ │ └──────────────┘ └──────────────┘ │ │ +│ └───────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +**内存预算:** + +| 组件 | 预估占用 | 说明 | +|------|----------|------| +| OS + 系统 | ~200MB | Alibaba Cloud Linux 3 | +| Docker | ~150MB | Docker Daemon | +| Gitea | ~200MB | 含 Act Runner | +| Nginx | ~50MB | 反向代理 | +| duoqi-api (prod) | ~300MB | 生产容器 | +| duoqi-api (test) | ~200MB | 测试容器(按需启停) | +| **合计** | **~1.1GB** | **剩余 ~900MB 缓冲** | + +--- + +## Phase 1: 单服务器方案(当前) + +### 资源规划 + +| 资源 | 配置 | 用途 | +|------|------|------| +| 轻量应用服务器 | 2C/2G/40GB SSD | Gitea + Docker + Nginx | +| 阿里云 RDS MySQL | 1C/1G 基础版 | duoqi_prod + duoqi_test | +| 域名 + SSL | api.duoqi.me | 生产环境入口 | + +### 服务器初始化 + +> 系统选择 **Alibaba Cloud Linux 3**(阿里云官方优化内核,内存占用更低,与 RDS 兼容性更好) + +```bash +# SSH 登录 +ssh root@your-server-ip + +# 1. 更新系统 +dnf update -y + +# 2. 安装基础工具 +dnf install -y curl git nginx certbot python3-certbot-nginx + +# 3. 安装 Docker(Alibaba Cloud Linux 内置 Docker 源) +dnf install -y docker docker-compose +systemctl enable --now docker + +# 4. 配置防火墙(firewalld) +# 所有服务通过 Nginx 反向代理,只需开放 22/80/443 +systemctl enable --now firewalld +firewall-cmd --permanent --add-service=ssh +firewall-cmd --permanent --add-service=http +firewall-cmd --permanent --add-service=https +firewall-cmd --reload + +# 5. 创建项目目录 +mkdir -p /opt/duoqi-api +mkdir -p /opt/gitea +mkdir -p /opt/backups + +# 6. 验证 +cat /etc/os-release # 确认 Alibaba Cloud Linux +docker --version +nginx -v +``` + +### Gitea 安装与配置 + +#### 方式一:二进制安装(推荐,更省内存) + +> Gitea 是单个 Go 二进制文件,直接运行在宿主机上比 Docker 方式省 ~50MB 内存,且 systemd 管理更可靠。 +> 在 2C/2G 服务器上,省下的每一 MB 都有意义。 + +```bash +# 下载 Gitea +wget -O /usr/local/bin/gitea https://dl.gitea.io/gitea/1.22/gitea-1.22-linux-amd64 +chmod +x /usr/local/bin/gitea + +# 创建用户和目录 +groupadd --system git +useradd --system --gid git --shell /bin/bash -m git +mkdir -p /var/lib/gitea/{custom,data,log} +chown -R git:git /var/lib/gitea +mkdir -p /etc/gitea +chown git:git /etc/gitea + +# 创建 systemd 服务 +cat > /etc/systemd/system/gitea.service << 'EOF' +[Unit] +Description=Gitea +After=network.target + +[Service] +User=git +WorkingDirectory=/var/lib/gitea +ExecStart=/usr/local/bin/gitea web -c /etc/gitea/app.ini --port 3200 --http-addr 127.0.0.1 +Restart=always +Environment=USER=git HOME=/home/git GITEA_WORK_DIR=/var/lib/gitea + +[Install] +WantedBy=multi-user.target +EOF + +# 启动 +systemctl enable gitea +systemctl start gitea + +# 等待 Gitea 生成默认配置文件 +sleep 3 +systemctl stop gitea +``` + +**配置反向代理适配(关键步骤):** + +> Gitea 需要知道自己对外是 `https://git.duoqi.me`,否则 Git clone URL 和 Web UI 链接会指向 `127.0.0.1:3200`。 + +```bash +# 编辑 Gitea 配置文件 +cat >> /etc/gitea/app.ini << 'EOF' + +[server] +DOMAIN = git.duoqi.me +ROOT_URL = https://git.duoqi.me/ +HTTP_ADDR = 127.0.0.1 +HTTP_PORT = 3200 +SSH_DOMAIN = git.duoqi.me +SSH_PORT = 22 +SSH_LISTEN_PORT = 22 +OFFLINE_MODE = true ; 禁用 Gravatar 等外部服务,加快页面加载 + +[security] +INSTALL_LOCK = true ; 已完成安装,禁止再次访问安装页面 + +[service] +REGISTER_EMAIL_CONFIRM = false ; 按需开启 +DISABLE_REGISTRATION = true ; 禁止公开注册,仅管理员创建账号 + +[other] +SHOW_FOOTER_VERSION = false ; 隐藏版本号,减少信息泄露 +EOF + +# 修正权限 +chown git:git /etc/gitea/app.ini +chmod 640 /etc/gitea/app.ini + +# 启动 +systemctl start gitea +``` + +#### 方式二:Docker 部署(备选) + +```bash +cat > /opt/gitea/docker-compose.yml << 'EOF' +version: '3.8' + +services: + gitea: + image: gitea/gitea:latest + container_name: gitea + restart: unless-stopped + environment: + - USER_UID=1000 + - USER_GID=1000 + - GITEA__server__DOMAIN=git.duoqi.me + - GITEA__server__ROOT_URL=https://git.duoqi.me/ + - GITEA__server__HTTP_ADDR=127.0.0.1 + - GITEA__server__HTTP_PORT=3200 + - GITEA__server__SSH_DOMAIN=git.duoqi.me + volumes: + - ./data:/data + - /etc/localtime:/etc/localtime:ro + network_mode: host ; 需要绑定 127.0.0.1,使用 host 网络 + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" +EOF + +cd /opt/gitea && docker-compose up -d +``` + +> **注意**: Docker 方式会额外占用 ~50MB 内存(容器运行时开销),仅当需要快速试用或测试时使用。 + +#### Gitea 初始化配置 + +``` +1. 浏览器访问 https://git.duoqi.me(通过 Nginx 反向代理) + 首次启动时也可临时访问 http://127.0.0.1:3200 +2. 首次配置(安装页面): + - 数据库:SQLite3(默认) + - 服务器域名:git.duoqi.me + - Gitea HTTP 监听端口:3200 + - SSH 服务端口:22 + - 管理员账号:创建 admin 用户 +3. 创建仓库:duoqi-api +4. 推送代码(HTTP): + git remote add gitea https://git.duoqi.me/admin/duoqi-api.git + git push gitea main + git checkout -b develop + git push gitea develop + + 或使用 SSH: + git remote add gitea git@git.duoqi.me:admin/duoqi-api.git + git push gitea main + git checkout -b develop + git push gitea develop +``` + +#### 安装 Act Runner(CI/CD 执行器) + +```bash +# 下载 Act Runner +wget -O /usr/local/bin/act_runner https://gitea.com/gitea/act_runner/releases/latest/download/act_runner-linux-amd64 +chmod +x /usr/local/bin/act_runner + +# 在 Gitea Web UI 中生成 Token: +# Settings → Actions → Runners → Create new Runner → 复制 Token + +# 创建专用工作目录(register 和 daemon 必须在同一目录) +mkdir -p /opt/act-runner + +# 注册 Runner +cd /opt/act-runner && act_runner register \ + --instance http://localhost:3200 \ + --token YOUR_RUNNER_TOKEN \ + --name duoqi-runner \ + --labels ubuntu-latest:docker://oven/bun:latest + +# 创建 systemd 服务 +cat > /etc/systemd/system/act-runner.service << 'EOF' +[Unit] +Description=Gitea Act Runner +After=docker.service + +[Service] +WorkingDirectory=/opt/act-runner +ExecStart=/usr/local/bin/act_runner daemon +Restart=always +Environment=HOME=/root + +[Install] +WantedBy=multi-user.target +EOF + +# 启动 +systemctl daemon-reload +systemctl enable act-runner +systemctl start act-runner + +# 验证:确认 active (running),且 Gitea Web UI 中 Runner 显示在线 +systemctl status act-runner +``` + +### 环境隔离策略 + +#### RDS 数据库隔离 + +```sql +-- 连接到 RDS +mysql -h your-rds-endpoint -u root -p + +-- 创建生产库 +CREATE DATABASE duoqi_prod CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- 创建测试库 +CREATE DATABASE duoqi_test CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- 创建用户并授权 +CREATE USER 'duoqi_prod'@'%' IDENTIFIED BY 'prod-password'; +GRANT ALL PRIVILEGES ON duoqi_prod.* TO 'duoqi_prod'@'%'; + +CREATE USER 'duoqi_test'@'%' IDENTIFIED BY 'test-password'; +GRANT ALL PRIVILEGES ON duoqi_test.* TO 'duoqi_test'@'%'; + +FLUSH PRIVILEGES; +``` + +#### 环境配置文件 + +**生产环境** `/opt/duoqi-api/.env.prod`: + +```env +DATABASE_URL=mysql://duoqi_prod:prod-password@your-rds-endpoint:3306/duoqi_prod +JWT_SECRET=prod-super-secret-jwt-key +JWT_EXPIRES_IN=1h +JWT_REFRESH_EXPIRES_IN=30d +ADMIN_TOKEN=prod-admin-token +PORT=3000 +NODE_ENV=production +LOG_LEVEL=warn +# ... 其他生产环境变量 +``` + +**测试环境** `/opt/duoqi-api/.env.test`: + +```env +DATABASE_URL=mysql://duoqi_test:test-password@your-rds-endpoint:3306/duoqi_test +JWT_SECRET=test-secret-key +JWT_EXPIRES_IN=1h +JWT_REFRESH_EXPIRES_IN=30d +ADMIN_TOKEN=test-admin-token +PORT=3001 +NODE_ENV=test +LOG_LEVEL=debug +# ... 其他测试环境变量(可使用测试用的华为、OSS配置) +``` + +#### Docker Compose 环境隔离 + +**服务器 compose 文件** `/opt/duoqi-api/docker-compose.yml`: + +```yaml +version: '3.8' + +services: + # ===== 生产环境 ===== + api-prod: + build: + context: /opt/gitea/data/git/repositories/admin/duoqi-api.git + dockerfile: Dockerfile + image: duoqi-api:prod + container_name: duoqi-api-prod + restart: unless-stopped + env_file: .env.prod + ports: + - "3000:3000" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + deploy: + resources: + limits: + memory: 400M + + # ===== 测试环境(按需启停) ===== + api-test: + build: + context: /opt/gitea/data/git/repositories/admin/duoqi-api.git + dockerfile: Dockerfile + image: duoqi-api:test + container_name: duoqi-api-test + restart: "no" # 不自动重启,手动控制 + env_file: .env.test + ports: + - "3001:3001" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3001/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + logging: + driver: "json-file" + options: + max-size: "5m" + max-file: "2" + deploy: + resources: + limits: + memory: 300M + profiles: + - test # 使用 profiles 按需启停 +``` + +**启停命令:** + +```bash +# 只启动生产环境(默认) +docker-compose up -d + +# 启动生产 + 测试环境 +docker-compose --profile test up -d + +# 停止测试环境(释放内存) +docker-compose --profile test stop api-test + +# 重新构建并启动 +docker-compose up -d --build api-prod +docker-compose --profile test up -d --build api-test +``` + +### CI/CD 流程 + +#### 双分支工作流 + +``` +develop 分支(开发测试) main 分支(生产发布) + │ │ + ▼ ▼ +┌───────────┐ ┌───────────┐ +│ quality │ ← Lint + 类型检查 │ quality │ ← Lint + 类型检查 +└─────┬─────┘ └─────┬─────┘ + ▼ ▼ +┌───────────┐ ┌───────────┐ +│ test │ ← 单元测试 + 覆盖率 │ test │ ← 单元测试 + 覆盖率 +└─────┬─────┘ └─────┬─────┘ + ▼ ▼ +┌────────────┐ ┌────────────┐ +│ build-test │ ← 构建测试镜像 │ build-prod │ ← 构建生产镜像 +└─────┬──────┘ └─────┬──────┘ + ▼ ▼ +┌─────────────┐ ┌─────────────┐ +│ deploy-test │ ← 自动部署到测试 │ deploy-prod │ ← 手动确认后部署生产 +└─────────────┘ └─────────────┘ +``` + +#### 日常开发流程 + +```bash +# 1. 在 develop 上开发 +git checkout develop +# ... 编写代码 ... +git add . +git commit -m "feat: 新功能描述" +git push origin develop +# → 自动触发:quality → test → build-test → deploy-test + +# 2. 在测试环境验证(http://test-api.duoqi.me) +# 如果发现问题,继续在 develop 上修改并 push + +# 3. 测试通过,合并到 main 发布 +git checkout main +git merge develop +git push origin main +# → 自动触发:quality → test → build-prod +# → 在 Gitea Web UI 手动确认 deploy-prod +``` + +#### 触发规则 + +| 事件 | quality | test | build | deploy | +|------|---------|------|-------|--------| +| push 到 `develop` | ✅ | ✅ | build-test | deploy-test(自动) | +| push 到 `main` | ✅ | ✅ | build-prod | deploy-prod(**手动确认**) | + +> **注意**:`develop` 分支需要在 Gitea 仓库中手动创建: +> ```bash +> git checkout -b develop +> git push origin develop +> ``` + +#### 关键设计说明 + +| 设计决策 | 原因 | +|---------|------| +| 双分支隔离(develop + main) | develop 是试验场,main 是稳定版本,互不干扰 | +| 不同分支构建不同镜像 | develop 只构建 test 镜像,main 只构建 prod 镜像,节省 2C/2G 服务器资源 | +| 本地构建镜像,无外部仓库 | 单服务器不需要镜像中转,节省资源 | +| 测试环境使用 Docker profiles | 按需启停,节省内存 | +| 生产部署手动确认 | 防止误操作,确保人工验证后才上线 | +| 使用 Gitea Actions | 兼容 GitHub Actions 语法,学习成本低 | + +### 部署操作 + +#### 手动部署(首次或紧急) + +```bash +# SSH 登录服务器 +ssh root@your-server-ip +cd /opt/duoqi-api + +# 从 Gitea 拉取最新代码 +git -C /opt/duoqi-api/repo pull # 如果用 git clone 方式 + +# 或使用部署脚本 +bash scripts/deploy.sh prod +``` + +#### 使用部署脚本 + +```bash +# 部署到生产环境 +bash scripts/deploy.sh prod + +# 部署到测试环境 +bash scripts/deploy.sh test + +# 停止测试环境 +bash scripts/deploy.sh test-stop + +# 查看状态 +bash scripts/deploy.sh status + +# 回滚 +bash scripts/deploy.sh rollback prod +``` + +### Nginx 配置 + +> 所有服务通过 Nginx 统一入口,后端端口仅绑定 localhost,防火墙只开 22/80/443。 + +```bash +# 生产环境 API +cat > /etc/nginx/conf.d/duoqi-api.conf << 'EOF' +server { + listen 80; + server_name api.duoqi.me; + + location / { + proxy_pass http://localhost:3000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +EOF + +# 测试环境 API(仅限内部访问) +cat > /etc/nginx/conf.d/duoqi-api-test.conf << 'EOF' +server { + listen 80; + server_name test-api.duoqi.me; + + # 限制访问 IP(开发团队办公网络) + # allow 123.56.78.90; + # deny all; + + location / { + proxy_pass http://localhost:3001; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +EOF + +# Gitea 代码仓库(限制访问 IP,仅团队可访问) +cat > /etc/nginx/conf.d/gitea.conf << 'EOF' +server { + listen 80; + server_name git.duoqi.me; + + # 限制访问 IP(开发团队办公网络) + # allow 123.56.78.90; + # deny all; + + location / { + proxy_pass http://127.0.0.1:3200; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Gitea 需要较大的请求体(git push) + client_max_body_size 100M; + } +} +EOF + +# 验证并生效 +nginx -t +systemctl enable --now nginx +systemctl reload nginx +``` + +#### 配置 HTTPS + +```bash +# 为三个域名统一申请证书 +certbot --nginx -d api.duoqi.me -d test-api.duoqi.me -d git.duoqi.me + +# 自动续期验证 +certbot renew --dry-run +``` + +> [!NOTE] +> +> 在子域名还没有配置解析的前提下,会影响证书的签发。后期如果新增子域名,并且需要同一张证书时,可以通过`--expand`参数进行申请。 + +```bash +# 方案1:先申请已就绪的子域名 +sudo certbot --nginx -d example.com -d www.example.com + +# 后续子域名就绪后,再扩容证书 +sudo certbot --nginx --expand \ + -d example.com -d www.example.com -d api.example.com -d blog.example.com +``` + +##### 常见错误 + +```bash +# ❌ 错误:只写新增域名,会导致原有域名被移除! +sudo certbot --nginx --expand -d api.example.com -d blog.example.com + +# ✅ 正确:列出所有域名(原有 + 新增) +sudo certbot --nginx --expand \ + -d example.com -d www.example.com -d api.example.com -d blog.example.com +``` + +--- + +## Phase 2: 多服务器扩展(未来) + +### 架构演进 + +``` +Phase 1 (当前) Phase 2 (扩展后) +┌──────────────┐ ┌─────────────────────────────────┐ +│ 单台服务器 │ │ 负载均衡 (SLB/Nginx) │ +│ │ │ api.duoqi.me → 443/80 │ +│ Gitea │ └──────────┬──────────────────────┘ +│ duoqi-api │ ──────▶ │ +│ duoqi-api │ ┌──────────┴──────────┐ +│ (prod+test) │ │ │ +│ Nginx │ ┌────┴────┐ ┌─────┴────┐ +└──────────────┘ │ API-1 │ │ API-2 │ + │ (prod) │ │ (prod) │ +┌──────────────┐ └─────────┘ └──────────┘ +│ RDS MySQL │ ┌─────────────┐ ┌──────────────┐ +│ duoqi_prod │ │ CI/CD 服务器 │ │ 测试服务器 │ +│ duoqi_test │ │ Gitea │ │ duoqi-api │ +└──────────────┘ │ Harbor 镜像 │ │ (test) │ + └─────────────┘ └──────────────┘ +``` + +### 扩展触发条件 + +当出现以下情况时,考虑从 Phase 1 升级到 Phase 2: + +| 指标 | Phase 1 上限 | Phase 2 起步 | +|------|-------------|-------------| +| 日活用户 | ~1,000 | >1,000 | +| 内存使用率 | 持续 >80% | — | +| CPU 使用率 | 持续 >70% | — | +| 需要独立测试服务器 | 否 | 是 | +| 需要多实例高可用 | 否 | 是 | + +### 私有镜像仓库 + +多服务器场景下需要镜像仓库统一分发: + +**方案一:Gitea 内置 Container Registry(推荐)** + +``` +# 在 Gitea 的 app.ini 中启用 +[packages] +ENABLED = true +STORAGE_TYPE = local + +# 推送镜像 +docker build -t your-server-ip:3200/admin/duoqi-api:latest . +docker push your-server-ip:3200/admin/duoqi-api:latest +``` + +**方案二:Harbor(大规模场景)** + +```yaml +# docker-compose.harbor.yml +# 需要独立服务器或至少 4GB 内存 +services: + harbor: + image: goharbor/harbor:latest + # ... Harbor 企业级镜像仓库 +``` + +### 负载均衡与水平扩展 + +```yaml +# Phase 2: 多实例 docker-compose.yml +# API 服务器上的配置 + +version: '3.8' + +services: + api: + image: your-registry/duoqi-api:latest + deploy: + replicas: 2 # 运行 2 个实例 + resources: + limits: + cpus: '1' + memory: 512M + env_file: .env.prod + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/health"] + interval: 15s + timeout: 5s + retries: 3 +``` + +**阿里云 SLB 配置:** + +``` +1. 创建负载均衡实例 +2. 添加后端服务器(API-1, API-2) +3. 配置健康检查路径:/health +4. 配置 SSL 证书 +5. 域名 DNS 解析到 SLB 公网 IP +``` + +### Phase 2 CI/CD 流程 + +``` +推送代码 → Gitea Actions → 构建 + 推送到私有镜像仓库 + │ + ┌───────────────┼───────────────┐ + ▼ ▼ ▼ + 部署到测试服务器 部署到 API-1 部署到 API-2 + (自动) (手动/灰度) (手动/灰度) +``` + +--- + +## 运维管理 + +### 日常维护命令 + +```bash +# 查看所有容器状态 +docker-compose ps + +# 查看生产日志 +docker-compose logs -f api-prod + +# 查看测试日志 +docker-compose -f docker-compose.yml --profile test logs -f api-test + +# 重启生产环境 +docker-compose restart api-prod + +# 进入容器调试 +docker-compose exec api-prod sh + +# 查看资源使用 +docker stats --no-stream +``` + +### 数据库管理 + +```bash +# 测试库重置(清空测试数据) +mysql -h your-rds-endpoint -u duoqi_test -p -e "DROP DATABASE duoqi_test; CREATE DATABASE duoqi_test CHARACTER SET utf8mb4;" + +# 执行迁移 +docker-compose exec api-prod npx drizzle-kit migrate + +# 导入种子数据到测试库 +docker-compose --profile test exec api-test bun run db:seed + +# 备份生产库 +mysqldump -h your-rds-endpoint -u duoqi_prod -p duoqi_prod > /opt/backups/duoqi_prod_$(date +%Y%m%d).sql +``` + +### 备份策略 + +```bash +# 创建备份脚本 +cat > /opt/backups/backup.sh << 'SCRIPT' +#!/bin/bash +set -e + +BACKUP_DIR="/opt/backups" +DATE=$(date +%Y%m%d_%H%M%S) + +# 数据库备份 +mysqldump -h your-rds-endpoint -u duoqi_prod -pyour-password duoqi_prod \ + | gzip > "$BACKUP_DIR/db_prod_$DATE.sql.gz" + +# Gitea 仓库备份 +sudo -u git gitea dump -c /etc/gitea/app.ini +# 备份文件生成在当前目录:gitea-dump-*.zip + +# 保留最近 7 天的备份 +find $BACKUP_DIR -name "*.gz" -mtime +7 -delete + +echo "[$DATE] Backup completed" +SCRIPT + +chmod +x /opt/backups/backup.sh + +# 定时任务(每天凌晨 2 点) +echo "0 2 * * * /opt/backups/backup.sh >> /opt/backups/backup.log 2>&1" | crontab - +``` + +--- + +## 故障排查 + +### 常见问题 + +#### 1. 内存不足 + +```bash +# 查看内存使用 +free -h + +# 停止测试环境释放内存 +docker-compose --profile test stop api-test + +# 清理 Docker 缓存 +docker system prune -f + +# 查看 Docker 磁盘占用 +docker system df +``` + +#### 2. 容器启动失败 + +```bash +# 查看退出日志 +docker-compose logs api-prod + +# 检查环境变量 +docker-compose config + +# 检查端口冲突 +netstat -tlnp | grep -E '3000|3001' +``` + +#### 3. Gitea Runner 不可用 + +```bash +# 检查 Runner 状态 +systemctl status act-runner + +# 重启 Runner +systemctl restart act-runner + +# 查看日志 +journalctl -u act-runner -f +``` + +#### 4. 数据库连接失败 + +```bash +# 从服务器测试 RDS 连通性 +mysql -h your-rds-endpoint -u duoqi_prod -p -e "SELECT 1;" + +# 检查 RDS 白名单是否包含服务器 IP +# 阿里云 RDS 控制台 → 数据安全性 → 白名单设置 +``` + +### 回滚操作 + +```bash +# 查看本地镜像历史 +docker images | grep duoqi-api + +# 回滚到上一个版本 +cd /opt/duoqi-api +docker-compose down api-prod +# 修改 docker-compose.yml 使用指定版本镜像 +docker-compose up -d api-prod +``` + +--- + +## 附录 + +### A. 完整目录结构(服务器端) + +``` +/opt/ +├── gitea/ # Gitea 代码托管 +│ ├── docker-compose.yml +│ └── data/ # Gitea 数据(仓库、配置) +├── duoqi-api/ # 应用部署 +│ ├── docker-compose.yml # 包含 prod + test 配置 +│ ├── .env.prod # 生产环境变量 +│ ├── .env.test # 测试环境变量 +│ ├── Dockerfile # 镜像构建 +│ └── repo/ # Git 仓库(用于构建) +├── backups/ # 备份目录 +│ ├── backup.sh +│ └── *.sql.gz +└── scripts/ # 运维脚本 + └── deploy.sh +``` + +### B. Gitea Actions 与 GitHub Actions 语法对照 + +| 特性 | GitHub Actions | Gitea Actions | +|------|---------------|---------------| +| 文件路径 | `.github/workflows/` | `.gitea/workflows/` | +| 语法 | YAML | 相同(完全兼容) | +| 触发条件 | `on: push` | 相同 | +| Runner | GitHub 托管 | 自托管 Act Runner | +| 镜像 | `runs-on: ubuntu-latest` | 相同,或自定义标签 | + +### C. 安全建议 + +| 项目 | 建议 | +|------|------| +| SSH | 禁用密码登录,仅密钥认证 | +| 防火墙 | firewalld 仅开放 22/80/443,后端端口全部绑定 localhost | +| 测试环境 | 通过 Nginx 限制访问 IP | +| 数据库 | 生产用户和测试用户严格分离权限 | +| JWT | 生产与测试使用不同密钥 | +| SSL | 生产环境必须启用 HTTPS | +| 内核更新 | Alibaba Cloud Linux 安全补丁自动推送,及时更新 | + +### D. Ubuntu 与 Alibaba Cloud Linux 命令速查 + +| 操作 | Ubuntu (apt) | Alibaba Cloud Linux (dnf) | +|------|-------------|--------------------------| +| 更新系统 | `apt update && apt upgrade -y` | `dnf update -y` | +| 安装软件 | `apt install -y ` | `dnf install -y ` | +| 安装 Docker | `curl -fsSL https://get.docker.com \| sh` | `dnf install -y docker` | +| 防火墙 | `ufw allow 80/tcp` | `firewall-cmd --permanent --add-port=80/tcp && firewall-cmd --reload` | +| 创建用户 | `adduser --system --group git` | `groupadd --system git && useradd --system --gid git git` | +| Nginx 配置 | `/etc/nginx/sites-available/` | `/etc/nginx/conf.d/` | + +--- + +**文档版本**: v5.0.0 (双分支工作流) +**最后更新**: 2026-04-16 +**维护者**: Duoqi Team diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100644 index 0000000..c761aad --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,105 @@ +#!/bin/bash +# Duoqi API 部署脚本 +# 用法:bash scripts/deploy.sh [命令] [环境] +# +# 命令: +# prod - 构建并部署生产环境 +# test - 构建并部署测试环境 +# test-stop - 停止测试环境(释放内存) +# status - 查看所有容器状态 +# rollback - 回滚生产环境到上一版本 +# logs - 查看生产日志 + +set -e + +DEPLOY_DIR="/opt/duoqi-api" +COMMAND="${1:-status}" +ENV="${2:-prod}" + +cd "$DEPLOY_DIR" 2>/dev/null || { + echo "Error: Deploy directory $DEPLOY_DIR not found" + echo "Please run server setup first." + exit 1 +} + +case "$COMMAND" in + prod) + echo "=== Building and deploying production ===" + docker-compose build api-prod + docker-compose up -d api-prod + echo "Waiting for health check..." + sleep 15 + if curl -sf http://localhost:3000/health > /dev/null; then + echo "✓ Production deployment successful!" + else + echo "✗ Production health check failed!" + exit 1 + fi + ;; + + test) + echo "=== Building and deploying test ===" + docker-compose build api-test + docker-compose --profile test up -d api-test + echo "Waiting for health check..." + sleep 10 + if curl -sf http://localhost:3001/health > /dev/null; then + echo "✓ Test deployment successful!" + else + echo "✗ Test health check failed!" + exit 1 + fi + ;; + + test-stop) + echo "=== Stopping test environment ===" + docker-compose --profile test stop api-test + echo "✓ Test environment stopped" + free -h | head -2 + ;; + + status) + echo "=== Container Status ===" + docker-compose ps + echo "" + echo "=== Memory Usage ===" + free -h | head -2 + echo "" + echo "=== Disk Usage ===" + df -h / | tail -1 + ;; + + rollback) + echo "=== Rolling back production ===" + if docker images | grep -q "duoqi-api:rollback"; then + docker tag duoqi-api:rollback duoqi-api:prod + docker-compose up -d --no-build api-prod + echo "✓ Rollback completed!" + else + echo "✗ No rollback image found!" + exit 1 + fi + ;; + + logs) + ENV="${2:-prod}" + if [ "$ENV" = "test" ]; then + docker-compose --profile test logs -f --tail=100 api-test + else + docker-compose logs -f --tail=100 api-prod + fi + ;; + + *) + echo "Usage: bash scripts/deploy.sh [command]" + echo "" + echo "Commands:" + echo " prod Build and deploy production" + echo " test Build and deploy test environment" + echo " test-stop Stop test environment (free memory)" + echo " status Show container status" + echo " rollback Rollback production" + echo " logs [env] View logs (prod/test, default: prod)" + exit 1 + ;; +esac