duoqi-admin/docs/ci-deployment-guide.md
Wang Zhuoxuan e0871d0b7a
Some checks failed
Build & Deploy Admin / deploy (push) Failing after 16s
docs: 添加部署方案文档与 Gitea Actions CI 配置
- 新增 CI 部署指南,包含 Runner 基础设施适配、HTTPS 证书实操经验、故障排查
- 新增 deploy.yml workflow,基于 Act Runner volume 挂载实现原子部署
2026-04-22 03:04:26 +08:00

536 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Duoqi Admin 部署方案
> 基于 duoqi-api 现有 Gitea + 阿里云轻量应用服务器基础设施,零额外内存的纯前端部署方案
## 目录
- [架构概览](#架构概览)
- [资源规划](#资源规划)
- [服务器端准备](#服务器端准备)
- [Nginx 配置](#nginx-配置)
- [HTTPS 证书](#https-证书)
- [DNS 配置](#dns-配置)
- [前端环境变量](#前端环境变量)
- [CI/CD 流程](#cicd-流程)
- [部署验证](#部署验证)
- [API 代理方案对比](#api-代理方案对比)
- [服务器端目录结构](#服务器端目录结构)
- [运维管理](#运维管理)
- [故障排查](#故障排查)
---
## 架构概览
### 核心设计
duoqi-admin 是纯前端 SPA不需要 Docker直接由 Nginx 托管静态文件。零额外内存开销,复用 duoqi-api 已有的 Gitea + Nginx + Act Runner 基础设施。
### 为什么不套 Docker
当前服务器 2GB 内存已用约 1.1GB。静态站点用 Docker 是浪费——Nginx 本身已在运行,直接加一个 `server` 块托管 `dist/` 即可,内存增量约等于 0。这也符合 duoqi-api 部署文档中"每 MB 都有意义"的设计原则。
### 架构图
```
┌─────────────────────────────────────────────────────────────────────┐
│ 阿里云轻量应用服务器 (2C/2G) │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ Nginx 反向代理 (:80/:443) │ │
│ │ ┌────────────┐ ┌──────────────┐ ┌────────┐ ┌──────────────┐ │ │
│ │ │api.duoqi.me│ │test-api. │ │git. │ │admin.duoqi.me│ │ │
│ │ │→ :3000 prod│ │duoqi.me │ │duoqi.me│ │→ 静态文件 │ │ │
│ │ │ │ │→ :3001 test │ │ │ │/opt/duoqi- │ │ │
│ │ │ │ │ │ │ │ │admin/dist/ │ │ │
│ │ └────────────┘ └──────────────┘ └────────┘ └──────────────┘ │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────────────────┐ │
│ │ duoqi-api │ │ duoqi-api │ │ Gitea + Act Runner │ │
│ │ (prod) │ │ (test) │ │ + duoqi-admin 仓库 │ │
│ │ :3000 │ │ :3001 │ │ CI/CD: build → copy │ │
│ └─────────────┘ └──────────────┘ └──────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ /opt/duoqi-admin/dist/ ← 构建产物(纯静态文件) │ │
│ └───────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
```
---
## 资源规划
### 内存增量:约 0MB
| 组件 | 新增占用 | 说明 |
|------|----------|------|
| Nginx server 块 | ~0MB | Nginx 已运行,静态文件不占额外进程内存 |
| dist/ 磁盘 | ~5-10MB | Vite 构建产物极小 |
| CI/CD | ~0MB | 复用已有 Act Runner |
### DNS 记录
| 子域名 | 类型 | 值 |
|--------|------|----|
| admin.duoqi.me | A | 服务器公网 IP |
---
## 服务器端准备
```bash
# 创建部署目录
mkdir -p /opt/duoqi-admin/dist
# 设置初始占位页面(首次部署前)
cat > /opt/duoqi-admin/dist/index.html << 'EOF'
<!DOCTYPE html>
<html><body><h1>Duoqi Admin - Deploying...</h1></body></html>
EOF
```
---
## Nginx 配置
创建配置文件 `/etc/nginx/conf.d/duoqi-admin.conf`
```nginx
server {
listen 80;
server_name admin.duoqi.me;
# 限制访问 IP仅管理员网络上线后取消注释
# allow 123.56.78.90;
# deny all;
root /opt/duoqi-admin/dist;
index index.html;
# SPA 路由回退 — 所有未匹配路径返回 index.html
location / {
try_files $uri $uri/ /index.html;
}
# API 代理 — 转发到后端生产环境
location /admin/ {
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;
}
# 静态资源缓存Vite 带 hash 的文件)
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# 安全头
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
}
```
验证并生效:
```bash
nginx -t
systemctl reload nginx
```
> **`try_files` 说明**React Router 的前端路由(如 `/questions`、`/categories`)在浏览器刷新时会向服务器请求这些路径。没有 `try_files` 回退Nginx 会返回 404。有了它所有未知路径都回退到 `index.html`,由前端 JS 路由接管。`/assets/` 目录下的文件由 Vite 自动添加 content hash如 `assets/index-DK2R8j.css`),可以安全设置 1 年强缓存。
---
## HTTPS 证书
将 admin.duoqi.me 加入已有证书(注意 `--expand` 必须列出所有域名):
```bash
sudo certbot --nginx --expand \
-d api.duoqi.me -d test-api.duoqi.me -d git.duoqi.me -d admin.duoqi.me
# 验证自动续期
certbot renew --dry-run
```
### `--expand` 注意事项
> **来自 duoqi-api 部署实操的经验**
1. **DNS 必须先生效**`--expand` 要求新域名的 DNS 已指向服务器且可公网访问,否则 certbot 的 HTTP-01 验证会失败。
2. **必须列出所有域名**`--expand` 是替换整张证书,不是追加。
```bash
# ❌ 错误:只写新增域名,会导致原有域名被移除!
sudo certbot --nginx --expand -d admin.duoqi.me
# ✅ 正确:列出所有域名(原有 + 新增)
sudo certbot --nginx --expand \
-d api.duoqi.me -d test-api.duoqi.me -d git.duoqi.me -d admin.duoqi.me
```
3. **备案问题**:阿里云服务器 + 未备案域名HTTP 请求可能被运营商层拦截(返回 403`Server: Beaver`),导致 certbot 验证失败。需要先完成 ICP 备案或暂时使用其他验证方式DNS-01
---
## DNS 配置
在域名管理后台添加 A 记录:
| 主机记录 | 记录类型 | 记录值 |
|----------|----------|--------|
| admin | A | 服务器公网 IP |
---
## 前端环境变量
构建时需指向生产 API。推荐使用 Nginx 代理方案,构建时 `VITE_API_BASE_URL` 留空:
```env
# .env.production
VITE_API_BASE_URL=
```
这样 `ky` 的请求会发到 `admin.duoqi.me/admin/*`,由 Nginx 转发到 `localhost:3000`,无跨域问题。
如果使用直连方案(不推荐):
```env
# .env.production直连方案
VITE_API_BASE_URL=https://api.duoqi.me
```
---
## CI/CD 流程
### 前置条件Runner 基础设施
> duoqi-admin 复用 duoqi-api 已搭建的 Gitea + Act Runner 基础设施。
> 以下配置需要在 Runner 端**一次性完成**,不需要每次部署时重复操作。
#### 1. Runner 配置文件更新
Runner 的 job 容器需要能访问 `/opt/duoqi-admin` 目录。编辑 `/opt/act-runner/config.yaml`
```yaml
container:
# 容器使用宿主机网络(继承自 duoqi-api 配置)
network: "host"
# 在原有挂载基础上,增加 admin 目录
options: "-v /opt/duoqi-api:/opt/duoqi-api -v /opt/duoqi-admin:/opt/duoqi-admin"
# 在原有白名单基础上,增加 admin 目录
valid_volumes:
- /opt/duoqi-api
- /opt/duoqi-admin
# 不强制每次拉取镜像(国内网络下减少失败风险)
force_pull: false
```
修改后重启 Runner
```bash
systemctl restart act-runner
```
> **为什么不用 SSH/rsync**Act Runner 的 job 容器通过 `network: host` + volume 挂载直接访问宿主机文件系统,无需 SSH 跳转。SSH 方式在容器内需要额外配置密钥,反而更复杂。
#### 2. 自定义 Runner 镜像
Runner 使用自定义镜像 `duoqi-runner:bun-git`(基于 `oven/bun` + git + docker CLI。该镜像
- **包含**bun、gitcheckout 需要、docker CLI
- **不包含**curl、rsync、ssh 客户端
- **固定版本**Bun 1.3,确保 CI 环境可复现
因此 CI 中不能使用 `curl`、`rsync`、`ssh` 等命令,需用 `bun`/`cp`/`mv` 替代。
#### 3. github_mirror 配置
Runner 已配置 `github_mirror: 'https://gitea.com'`,从 gitea.com 镜像拉取 GitHub Actions`actions/checkout`),解决国内无法访问 GitHub 的问题。如果 Action 拉取失败:
```bash
# 清除缓存的 action修复损坏的缓存
rm -rf /root/.cache/act/
systemctl restart act-runner
```
### Pipeline 流程
```
推送代码到 Gitea (main 分支)
┌───────────────┐
│ quality │ ← Lint + 类型检查
│ (自动触发) │
└───────┬───────┘
┌───────────────┐
│ build │ ← bun run build → dist/
│ (自动触发) │
└───────┬───────┘
┌───────────────┐
│ deploy │ ← 原子替换 dist/ 目录
│ (手动触发) │
└───────────────┘
```
### Gitea Actions 配置
创建 `.gitea/workflows/deploy.yml`
```yaml
name: Build & Deploy Admin
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Lint & Type check
run: |
bun run lint
bun run typecheck
- name: Build
run: bun run build
env:
VITE_API_BASE_URL: ""
- name: Deploy to server
run: |
# 原子替换:通过 volume 挂载直接操作宿主机目录
mkdir -p /opt/duoqi-admin/dist-new
cp -r dist/* /opt/duoqi-admin/dist-new/
# 备份当前版本 + 切换mv 在同一文件系统上是原子操作)
mv /opt/duoqi-admin/dist /opt/duoqi-admin/dist-old || true
mv /opt/duoqi-admin/dist-new /opt/duoqi-admin/dist
rm -rf /opt/duoqi-admin/dist-old
```
> **原子替换说明**:直接 `rm -rf dist && cp` 在中间有个时间窗口,用户会看到 404 或不完整文件。用 `dist-new → mv swap → rm old` 三步替换,`mv` 是原子操作(同一文件系统),切换瞬间完成,零停机。
>
> **与 duoqi-api 的区别**duoqi-api 通过 Docker 容器部署CI 中需要 `docker build`/`docker compose`。duoqi-admin 是纯静态文件CI 中只需 `cp` + `mv` 操作,更轻量。
---
## 部署验证
### 构建验证
```bash
# 本地构建
bun run build
# 本地预览构建产物
bun run preview
```
### 线上验证
```bash
# 检查首页可访问
curl -I https://admin.duoqi.me
# 检查静态资源缓存头
curl -I https://admin.duoqi.me/assets/
# 功能验证(浏览器)
# 1. 打开 https://admin.duoqi.me
# 2. 登录
# 3. 执行 CRUD 操作
# 4. 验证 API 连通(浏览器 DevTools → Network
```
---
## API 代理方案对比
| 方案 | 优点 | 缺点 |
|------|------|------|
| **Nginx 代理(推荐)** | 无跨域问题、Cookie 共享、CDN 友好 | Nginx 配置多几行 |
| 前端直连 api.duoqi.me | 简单 | 需要后端配 CORS、浏览器发跨域预检请求 |
---
## 服务器端目录结构
```
/opt/
├── gitea/ # Gitea已有
├── duoqi-api/ # API 部署(已有)
│ ├── docker-compose.yml
│ ├── .env.prod / .env.test
│ └── repo/
├── duoqi-admin/ # ← 新增
│ ├── dist/ # 当前线上版本
│ ├── dist-new/ # (部署时临时存在)
│ └── dist-old/ # (部署后临时存在,自动清理)
├── backups/ # 备份(已有)
└── scripts/ # 运维脚本(已有)
```
---
## 运维管理
### 手动部署(首次或紧急)
```bash
# SSH 登录服务器
ssh root@your-server-ip
# 拉取最新代码
cd /opt/duoqi-admin/repo && git pull origin main
# 构建
bun install --frozen-lockfile
bun run build
# 原子替换
mkdir -p /opt/duoqi-admin/dist-new
cp -r dist/* /opt/duoqi-admin/dist-new/
mv /opt/duoqi-admin/dist /opt/duoqi-admin/dist-old
mv /opt/duoqi-admin/dist-new /opt/duoqi-admin/dist
rm -rf /opt/duoqi-admin/dist-old
```
### 回滚
```bash
# 保留最近一个版本的备份
# 回滚 = 将 dist-old 恢复
mv /opt/duoqi-admin/dist /opt/duoqi-admin/dist-failed
mv /opt/duoqi-admin/dist-old /opt/duoqi-admin/dist
rm -rf /opt/duoqi-admin/dist-failed
```
### 日常检查
```bash
# 检查 Nginx 状态
systemctl status nginx
# 检查站点响应
curl -s -o /dev/null -w "%{http_code}" https://admin.duoqi.me
# 检查磁盘占用
du -sh /opt/duoqi-admin/dist/
```
---
## 故障排查
### CI Action 拉取失败
**症状**`actions/checkout` 或 `oven-sh/setup-bun` 拉取超时或 404。
```bash
# 检查 github_mirror 配置
grep github_mirror /opt/act-runner/config.yaml
# 应输出github_mirror: 'https://gitea.com'
# 清除缓存的 action修复损坏的缓存
rm -rf /root/.cache/act/
systemctl restart act-runner
```
### CI checkout 失败ECONNREFUSED
**症状**job 容器无法连接 Gitea API。
```bash
# 检查容器网络配置
grep -A2 "network:" /opt/act-runner/config.yaml
# 应包含network: "host"
# 确认 Gitea API 可达
curl http://localhost:3200/api/v1/repos/search?q=duoqi-admin
```
### CI deploy 步骤找不到 /opt/duoqi-admin
**症状**`cp` 或 `mv` 报 No such file or directory。
```bash
# 检查 Runner 容器的 volume 挂载配置
grep "options:" /opt/act-runner/config.yaml
# 应包含:-v /opt/duoqi-admin:/opt/duoqi-admin
grep "valid_volumes:" -A3 /opt/act-runner/config.yaml
# 应包含:- /opt/duoqi-admin
```
### 白屏但 HTTP 200
**原因**JS 加载失败或路由未回退。
```bash
# 检查 Nginx try_files 配置
nginx -T | grep try_files
# 检查 dist/ 文件完整性
ls -la /opt/duoqi-admin/dist/
ls -la /opt/duoqi-admin/dist/assets/
```
### API 请求 404
**原因**`/admin/` 代理未生效。
```bash
# 检查 Nginx 代理配置
nginx -T | grep -A5 "location /admin/"
# 直接测试后端
curl http://localhost:3000/health
```
### 样式/JS 未更新
**原因**浏览器缓存。Vite 构建会自动给文件加 hash但如果 HTML 被缓存则不会加载新资源。
```bash
# 检查 index.html 的 Cache-Control
curl -I https://admin.duoqi.me/index.html
# 应该没有长缓存头index.html 不缓存,由 try_files 默认处理)
```
### HTTPS 证书问题
```bash
# 检查证书覆盖的域名
echo | openssl s_client -connect admin.duoqi.me:443 2>/dev/null | openssl x509 -noout -text | grep DNS
# 重新申请
sudo certbot --nginx --expand \
-d api.duoqi.me -d test-api.duoqi.me -d git.duoqi.me -d admin.duoqi.me
```
---
**文档版本**: v1.1.0
**最后更新**: 2026-04-22
**维护者**: Duoqi Team