Files
backend/docs/10-生产部署文档.md
T
34047007@qq.com a6cd99a4ca
CI / backend (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
feat: initial commit - oncology literature search platform
OncoLit: a multi-tenant oncology literature search, feed, and
collaboration platform. Built with FastAPI + Vue 3 + PostgreSQL.
Includes PubMed pipeline, drug approvals, AI summaries, and
systematic review tools.
2026-07-27 07:59:18 +08:00

39 KiB
Raw Blame History

SciLit Oncology — 生产部署文档

目录

  1. 前置准备
  2. 服务器基础环境
  3. 中间件安装
  4. 域名与SSL
  5. 应用部署
  6. 数据库初始化
  7. 第三方服务注册
  8. 上线验证清单
  9. 日常运维
  10. 应急预案

1. 前置准备

1.1 你需要准备好的东西

项目 说明 在哪里获取
☁️ 服务器 Linux (Ubuntu 22.04 推荐), 2C4G 起步,50G SSD 阿里云/腾讯云/AWS
🔑 SSH 密钥 用于免密登录服务器 ssh-keygen 本地生成,公钥上传到云控制台
🌐 域名 oncolit.gonsun.com 或你的域名 阿里云/腾讯云/Namecheap
📧 邮箱 用于 SMTP 发件和系统通知 阿里云邮件推送 / SendGrid

1.2 本地先确认项目能跑

cd d:/ClaudeCode/backend
python scripts/seed_data.py
uvicorn app.main:app --port 8000

cd d:/ClaudeCode/frontend
npm run build

2. 服务器基础环境

2.1 登录服务器

ssh root@<你的服务器IP>

# 创建非 root 用户
adduser scilit
usermod -aG sudo scilit
su - scilit

2.2 更新系统 + 安装基础工具

sudo apt update && sudo apt upgrade -y
sudo apt install -y curl wget git vim htop net-tools ufw

# 防火墙:只开放需要的端口
sudo ufw allow 22    # SSH
sudo ufw allow 80    # HTTP
sudo ufw allow 443   # HTTPS
sudo ufw enable

2.3 安装 Docker

curl -fsSL https://get.docker.com | sudo bash
sudo usermod -aG docker $USER
newgrp docker

# 安装 Docker Compose
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

# 验证
docker --version
docker-compose --version

2.4 安装 Nginx(宿主机,用于反向代理和 SSL)

sudo apt install -y nginx certbot python3-certbot-nginx

3. 中间件安装

3.1 方案选择

方案 适用场景 运维成本
A: 阿里云 RDS + Redis + ES 有预算,想省运维
B: Docker Compose 自建全部 小规模起步,成本敏感

下面按**方案 B(自建)**编写。方案 A 请参考各云服务商的快速入门文档。

3.2 PostgreSQL(自建,使用 pgvector 镜像支持未来向量搜索)

Docker Compose 中已定义,无需额外操作:

# docker-compose.prod.yml 中
postgres:
  image: pgvector/pgvector:pg16
  environment:
    POSTGRES_DB: scilit
    POSTGRES_USER: scilit
    POSTGRES_PASSWORD: ${PG_PASSWORD}  # 在 .env 中设置
  volumes:
    - /data/postgres:/var/lib/postgresql/data  # 持久化到宿主机
  restart: unless-stopped

3.3 Redis(自建)

redis:
  image: redis:7-alpine
  command: redis-server --appendonly yes  # 开启持久化
  volumes:
    - /data/redis:/data
  restart: unless-stopped

3.4 Elasticsearch(可选,搜索增强)

elasticsearch:
  image: elasticsearch:8.11.0
  environment:
    discovery.type: single-node
    xpack.security.enabled: "false"
    "ES_JAVA_OPTS": "-Xms512m -Xmx512m"
    bootstrap.memory_lock: "true"
  volumes:
    - /data/es:/usr/share/elasticsearch/data
  restart: unless-stopped
  ulimits:
    memlock: { soft: -1, hard: -1 }

小规模起步可以先不启 ESES_URL 为空时系统自动回退 PostgreSQL tsvector 搜索),等数据量达到 200-300 万条再启。启动方式:取消注释 docker-compose 中的 elasticsearch 服务段,然后运行 python scripts/es_index.py 全量重建索引。

3.5 MinIO(对象存储,存储用户文件 + 文献 PDF)

MinIO 是兼容 AWS S3 协议的自托管对象存储,用于存储用户头像、上传文件以及未来的文献 PDF 原文。

minio:
  image: minio/minio:latest
  command: server /data --console-address ":9001"
  environment:
    MINIO_ROOT_USER: minioadmin        # 在 .env 中用 MINIO_ROOT_USER 覆盖
    MINIO_ROOT_PASSWORD: minioadmin    # 在 .env 中用 MINIO_ROOT_PASSWORD 覆盖
  volumes:
    - /data/minio:/data
  restart: unless-stopped

MinIO Web 控制台端口为 9001API 端口为 9000。生产环境不建议对外暴露端口,通过后端代理访问。首次部署后登录控制台创建 scilit-files 桶(或后端自动创建)。

MinIO vs 阿里云 OSS 对比

MinIO(自建) 阿里云 OSS
成本 仅占磁盘空间 ¥0.12/GB/月 + 流量费
延迟 ~0.1ms(内网) 2-5ms
运维 需管理磁盘 托管
带宽 受服务器带宽限制 不限速
适用场景 小规模起步、内部使用 大规模、需 CDN 分发

决策记录(2026-07-09): 先用 MinIO 自托。主要考量:用户量初期不大、PDF 存储主要为内部解析 + 用户自行下载(需 VPN),MinIO 内网延迟更低,省云费用。

3.5 关于 Elasticsearch

docker-compose.prod.yml 中已配置 ES 8.11.0 容器,但生产环境默认不启用ES_URL 为空时系统自动回退 PostgreSQL tsvector 搜索)。如需启用,在 .env 中设置 ES_URL=http://elasticsearch:9200 即可。

ES 服务至少需要 2GB 内存(JVM heap 512MB-1GB),小规模部署(<200 万条文献)不需要 ES。


4. 域名与SSL

4.1 DNS 解析

在域名服务商后台添加 A 记录:

类型    主机记录    记录值
A       @          你的服务器IP
A       www        你的服务器IP

4.2 Nginx 配置

方案 A(推荐):前端 Nginx 容器docker-compose.prod.ymlfrontend 服务使用 Dockerfile.prod(基于 Nginx),自动处理静态文件托管 + API 反向代理。无需宿主机 Nginx。

# docker-compose.prod.yml 启动后,frontend 容器监听 80 端口
# 宿主机只需安装 Nginx 做 SSL 终止和域名转发:
sudo apt install -y nginx certbot python3-certbot-nginx
# /etc/nginx/sites-available/scilit-oncology
server {
    listen 80;
    server_name oncolit.gonsun.com www.oncolit.gonsun.com;

    location / {
        proxy_pass http://127.0.0.1:80;  # 转发到 frontend Nginx 容器
        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;
    }
}

方案 B(不推荐):宿主机 Nginx 直接托管前端文件

sudo vim /etc/nginx/sites-available/scilit-oncology
# /etc/nginx/sites-available/scilit-oncology
server {
    listen 80;
    server_name oncolit.gonsun.com www.oncolit.gonsun.com;

    # 前端静态文件
    location / {
        root /home/scilit/sci-lit-manager/frontend/dist;
        try_files $uri $uri/ /index.html;
    }

    # API 反代到 Docker 容器内的 FastAPI
    location /api/ {
        proxy_pass http://127.0.0.1:8000;
        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;
        proxy_read_timeout 120s;
    }

    # WebSocket
    location /api/v1/ws/ {
        proxy_pass http://127.0.0.1:8000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
    }

    # Swagger 文档
    location /docs {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
    }

    # 静态资源缓存
    location /assets/ {
        root /home/scilit/sci-lit-manager/frontend/dist;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
}
# 启用站点
sudo ln -s /etc/nginx/sites-available/scilit-oncology /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

4.3 SSL 证书(Let's Encrypt 免费)

sudo certbot --nginx -d oncolit.gonsun.com -d www.oncolit.gonsun.com
# 按提示输入邮箱,同意条款

# 设置自动续期
sudo certbot renew --dry-run

5. 应用部署

5.1 上传项目

# 本地打包(排除 node_modules 和 __pycache__
cd d:/ClaudeCode
tar -czf scilit-oncology.tar.gz \
    --exclude='node_modules' --exclude='__pycache__' \
    --exclude='*.db' --exclude='dist' \
    --exclude='.git' \
    backend/ frontend/ docker-compose.yml docker-compose.prod.yml \
    scripts/ CLAUDE.md README.md .github/ .env.example

# 上传到服务器
scp scilit-oncology.tar.gz scilit@<IP>:/home/scilit/

# 服务器端解压
ssh scilit@<IP>
cd /home/scilit
mkdir -p sci-lit-manager
tar -xzf scilit-oncology.tar.gz -C sci-lit-manager
cd sci-lit-manager

5.2 创建生产环境变量

# /home/scilit/sci-lit-manager/.env
vim .env
# ===== 数据库(必须修改) =====
PG_PASSWORD=<生成强密码: openssl rand -hex 16>
REDIS_PASSWORD=<生成强密码: openssl rand -hex 16>

# ===== JWT(必须修改) =====
JWT_SECRET=<运行 openssl rand -hex 32 生成>

# ===== S3 / MinIO =====
S3_ENDPOINT=http://minio:9000
S3_ACCESS_KEY=minioadmin
S3_SECRET_KEY=minioadmin
S3_BUCKET=scilit-files

# ===== 邮件(腾讯云邮件推送 SES =====
SMTP_HOST=smtp.qcloudmail.com
SMTP_PORT=465
SMTP_USER=service@oncolit.gonsun.com
SMTP_PASSWORD=<腾讯云SES SMTP密码>
SMTP_FROM=service@oncolit.gonsun.com

# ===== AI 摘要(DeepSeek =====
AI_API_KEY=sk-your-deepseek-api-key
AI_BASE_URL=https://api.deepseek.com/v1
AI_MODEL=deepseek-chat

# ===== PubMed =====
PUBMED_API_KEY=your-ncbi-api-key

# ===== 搜索 =====
ES_URL=http://elasticsearch:9200

# ===== URL 配置 =====
PUBLIC_BASE_URL=https://oncolit.gonsun.com
CORS_ORIGINS=["https://oncolit.gonsun.com"]

# ===== 专科配置 =====
SPECIALTY=oncology
DEBUG=false

5.3 构建 Docker 镜像并启动

cd /home/scilit/sci-lit-manager

# 使用生产环境 Docker Compose 文件
docker compose -f docker-compose.prod.yml --env-file .env up -d postgres redis

# 等待数据库就绪
sleep 10

# 运行数据库迁移(migrate 服务会自动执行,也可手动)
docker compose -f docker-compose.prod.yml run --rm migrate

# 加载种子数据
docker compose -f docker-compose.prod.yml run --rm backend python scripts/seed_data.py

# 启动全部服务
docker compose -f docker-compose.prod.yml --env-file .env up -d

# 查看日志确认正常
docker compose -f docker-compose.prod.yml logs -f backend

5.4 验证服务

# 健康检查
curl https://oncolit.gonsun.com/api/v1/health

# 应该返回
# {"status":"ok","specialty":"oncology","db":"ok","version":"0.1.0"}

# 公开API
curl https://oncolit.gonsun.com/api/v1/public/feed?page_size=1

# 登录测试
curl -X POST https://oncolit.gonsun.com/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"demo@test.cn","password":"123456"}'

6. 数据库初始化

6.1 首次部署

服务器上已经用 Docker Compose 启动了 PostgreSQL。首次初始化:

COMPOSE="docker compose -f /home/scilit/sci-lit-manager/docker-compose.prod.yml --env-file /home/scilit/sci-lit-manager/.env"

# 运行迁移
$COMPOSE run --rm migrate

# 导入种子数据
$COMPOSE run --rm backend python scripts/seed_data.py

6.2 数据备份

# 每日备份脚本
# /home/scilit/backup.sh
#!/bin/bash
BACKUP_DIR=/home/scilit/backups
mkdir -p $BACKUP_DIR
DATE=$(date +%Y%m%d_%H%M%S)
COMPOSE="docker compose -f /home/scilit/sci-lit-manager/docker-compose.prod.yml"

# 备份 PostgreSQL
$COMPOSE exec -T postgres pg_dump -U scilit scilit > $BACKUP_DIR/scilit_$DATE.sql

# 保留最近 7 天
find $BACKUP_DIR -name "*.sql" -mtime +7 -delete

echo "Backup completed: $DATE"
# 添加定时任务(每天凌晨 3 点备份)
crontab -e
# 添加:
0 3 * * * /home/scilit/backup.sh >> /home/scilit/backups/backup.log 2>&1

6.3 PubMed 基线数据导入

# 下载 PubMed 2025 基线(约 40GB
# 服务器上使用 aria2 多连接下载
aria2c -x 8 -s 8 https://ftp.ncbi.nlm.nih.gov/pubmed/baseline/pubmed25n0001.xml.gz

# 导入(仅肿瘤科相关,会自动过滤)
COMPOSE="docker compose -f /home/scilit/sci-lit-manager/docker-compose.prod.yml --env-file /home/scilit/sci-lit-manager/.env"
$COMPOSE exec backend python scripts/pubmed_baseline.py \
  --dir /data/pubmed/baseline \
  --demo  # 先试 5 个文件,确认没问题后去掉 --demo

7. 第三方服务注册

7.1 SMTP 邮件(腾讯云邮件推送 SES)

使用腾讯云邮件推送(Simple Email Service)发送通知/邀请邮件。SMTP 服务器 smtp.qcloudmail.com:465,密码需在腾讯云 SES 控制台生成 SMTP 密码。验证方式:

# 发送测试邮件
docker compose exec backend python -c "
from app.services.email import send_email
import asyncio
asyncio.run(send_email('your@email.com', 'Test', '<p>OK</p>'))
"

### 7.2 Stripe 支付

1. 注册 [stripe.com](https://stripe.com)(需要真实的公司/个人信息)
2. 先在 Test Mode 测试:
   - 获取 `sk_test_xxx``pk_test_xxx`
   - 在 Stripe Dashboard 创建 Products
     - Pro Monthly: ¥35/month
     - Team Monthly: ¥70/user/month
     - Enterprise: Custom quote
   - 获取 Price IDs
3. 生产上线前切换到 Live Mode
4. 配置 Webhook
   - URL: `https://oncolit.gonsun.com/api/v1/webhooks/stripe`
   - Events: `checkout.session.completed`, `customer.subscription.*`, `invoice.*`
   - 获取 `whsec_xxx` Signing Secret

### 7.3 AI APIDeepSeek,已配置)

已在 `.env` 中配置 DeepSeek API

```bash
AI_API_KEY=sk-f59fabc2884d49328505567518729685
AI_BASE_URL=https://api.deepseek.com/v1
AI_MODEL=deepseek-chat

如需切换为 OpenAI 或其他模型,修改 .env 中对应项即可。

7.4 对象存储(MinIO 自建)

已在 docker-compose.prod.yml 中配置 MinIO 服务。首次部署后操作:

# 访问 MinIO Web 控制台(先暴露端口调试)
# 默认:http://<服务器IP>:9001,账号 minioadmin / minioadmin

# 或通过 mc 客户端配置
docker compose exec minio mc alias local http://localhost:9000 minioadmin minioadmin
docker compose exec minio mc mb local/scilit-files

# 验证
docker compose exec minio mc ls local/scilit-files

安全提示: 初始账号密码(minioadmin/minioadmin)在生产环境应立即通过 .env 中的 MINIO_ROOT_USERMINIO_ROOT_PASSWORD 修改。

7.5 PubMed API Key + 数据填充(部署后一次性执行)

7.5.1 PubMed API Key

已在 .env 中配置,速率 10 req/s

PUBMED_API_KEY=692e319f33129b7a5d2d94c57b67739e0a08

如 Key 过期或被轮换,登录 NCBI Account → API Key Management 更新。

7.5.2 部署后一次性数据填充

按以下顺序执行(越靠前的越关键):

# 0. 获取管理员 Token
admin_token=$(curl -s -X POST https://oncolit.gonsun.com/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"admin@yourhospital.com","password":"你的密码"}' | python3 -c "import sys,json; print(json.load(sys.stdin)['token']['access_token'])")

# 1. 导入种子标签 + 期刊
COMPOSE="docker compose -f /home/scilit/sci-lit-manager/docker-compose.prod.yml --env-file /home/scilit/sci-lit-manager/.env"
$COMPOSE exec backend python scripts/seed_tags_only.py

# 2. 回填 MeSH 标签(将已有 mesh_headings 匹配到 GlobalTag
#    预期:~50% 的文献获得标签
curl -X POST "https://oncolit.gonsun.com/api/v1/admin/pipeline/retag-tags" \
  -H "Authorization: Bearer $admin_token"

# 3. 刷新引用次数(全库跑一次,约 15 分钟,3 req/s)
curl -X POST "https://oncolit.gonsun.com/api/v1/admin/pipeline/refresh-citations" \
  -H "Authorization: Bearer $admin_token"

# 4. 提取 PICO 要素(从摘要中抽取,调用 DeepSeek API3 并发)
curl -X POST "https://oncolit.gonsun.com/api/v1/admin/pipeline/extract-pico?limit=500" \
  -H "Authorization: Bearer $admin_token"

# 5. 生成 AI 一句话摘要(每次处理最近 30 篇有摘要的文献)
#    可运行多次,每次处理 30 篇
curl -X POST "https://oncolit.gonsun.com/api/v1/admin/ai/summarize" \
  -H "Authorization: Bearer $admin_token"

# 6. 回填 OA 全文(仅 ~0.5% 的文献有 PMC 全文,轻量)
curl -X POST "https://oncolit.gonsun.com/api/v1/admin/pipeline/backfill-oa-text?limit=5000" \
  -H "Authorization: Bearer $admin_token"

# 7. 回填研究设计分类(基于 PubType 映射)
curl -X POST "https://oncolit.gonsun.com/api/v1/admin/pipeline/backfill-study-designs" \
  -H "Authorization: Bearer $admin_token"

# 8. 触发一次完整的文献检索管道
curl -X POST "https://oncolit.gonsun.com/api/v1/admin/pipeline/run" \
  -H "Authorization: Bearer $admin_token"

部署后首次填充预期效果(Dashboard 数据质量面板可验证):

指标 预期值 说明
已打标文献 ~50% MeSH UI 匹配精度
PICO 覆盖 ~36% 有摘要的文献
引用次数 依文献年代 PubMed elink 实时查询。2026 年新文献引用数多为 0
OA 全文 ~0.5% PMC 平台全文
AI 摘要 30篇/次 可重复触发

定时任务说明: ARQ Worker 在 Docker Compose 启动后自动运行,无需手动配置。

  • 每日 03:07 UTC — 精搜
  • 每周日 03:37 UTC — 宽搜
  • 每日 05:13 UTC — 引用刷新
  • 每日 22:30 UTC — 摘要邮件

7.6 微信服务号

  1. 注册 微信公众平台
  2. 选择"服务号" → 提交营业执照 → 等待审核(1-7天)
  3. 认证费用 ¥300/年
  4. 认证后在"开发 → 基本配置"获取 AppID + AppSecret
  5. 在"功能 → 模板消息"申请模板(审核 1-3 天)
  6. 在项目中配置:
WECHAT_APP_ID=wx...
WECHAT_APP_SECRET=...
WECHAT_TEMPLATE_ID=...

8. 上线验证清单

8.1 安全

  • JWT_SECRET 已随机生成,不是 dev-secret-...
  • SSL 证书生效(浏览器显示 🔒
  • 防火墙只开放 22/80/443,数据库端口不对外
  • PostgreSQL 密码已改(非默认),Redis 密码已设
  • /admin 普通用户无法访问
  • S3/MinIO 桶已创建,应用可读写
  • 忘记密码功能发真实的邮件(不是 dev 模式的 reset_link

8.2 功能

  • 公开首页可访问,能浏览文献
  • 注册流程走通(验证码 → 注册 → 设置领域 → 看Feed)
  • Demo 账号能登录并看到 42 篇个性化推送
  • 管理员能登录 /admin 并看到看板数据
  • 搜索返回正确结果
  • 引用导出 BibTeX/RIS 正常
  • API 文档 /docs 可访问

8.3 数据管道

  • 种子标签已导入(seed_tags_only.py
  • MeSH 标签回填已执行(retag-tags
  • 引用次数已刷新(refresh-citations
  • PICO 提取已执行(extract-pico
  • AI 摘要已生成(ai/summarize
  • OA 全文已回填(backfill-oa-text
  • 管道定时任务已启动(ARQ Worker 存活)
  • Dashboard 数据质量面板各指标在预期范围内
  • 首次完整管道运行无异常

8.3 性能

  • 公开 Feed 首屏加载 < 1 秒
  • 登录后 Feed < 2 秒
  • API 响应有 X-Response-Time-Ms
  • 静态资源有 Cache-Control 头

8.5 数据库连接池

当前使用 SQLAlchemy 内置连接池(backend/app/db.py):

  • pool_size=10, max_overflow=20 → 单 worker 最大 30 连接
  • 4 个 uvicorn worker → 最大 120 个 PG 连接
  • PG 16 默认 max_connections=100,已超限

决定:暂不引入 PgBouncer。 当前用户量未达瓶颈,后续并发增长后需在应用层与 PG 之间添加 PgBouncertransaction 模式)。届时需下调 pool_size 避免连接堆积。

8.6 首页 Feed 缓存

首页文献列表使用预缓存策略(类似热搜缓存 hot_articles_cache.py)。

缓存 Key homepage:feed
TTL 1800s30 分钟)
刷新任务 refresh_homepage_feedARQ cron 每 0/30 分
降级策略 缓存未命中时走实时简化查询
预计算条数 50 条(返回 20 条)

设计意图: 避免每次首页加载走 POST /features/search/advanced 高级搜索全套流程(COUNT+SELECT+标签JOIN+期刊JOIN)。预计算只取 pub_date 索引的最新 50 条,不取 JSON 大字段(authors/mesh_headings/ai_summary),传输量从 400KB+ 降到 ~10KB。


9. 日常运维

9.1 查看日志

# 所有服务(使用生产 Compose 文件)
COMPOSE="docker compose -f /home/scilit/sci-lit-manager/docker-compose.prod.yml --env-file /home/scilit/sci-lit-manager/.env"
$COMPOSE logs -f

# 只看后端
$COMPOSE logs -f backend

# 最近 100 行
$COMPOSE logs --tail=100 backend

9.2 重启服务

COMPOSE="docker compose -f /home/scilit/sci-lit-manager/docker-compose.prod.yml --env-file /home/scilit/sci-lit-manager/.env"
$COMPOSE restart backend

9.3 更新代码

# 服务器
SSH_HOST=scilit@<服务器IP>
COMPOSE="docker compose -f /home/scilit/sci-lit-manager/docker-compose.prod.yml --env-file /home/scilit/sci-lit-manager/.env"

# 更新后端代码并重建
ssh $SSH_HOST "cd /home/scilit/sci-lit-manager && git pull"
ssh $SSH_HOST "$COMPOSE build backend && $COMPOSE up -d backend"

# 更新前端代码并重建
ssh $SSH_HOST "$COMPOSE build frontend && $COMPOSE up -d frontend"

9.4 数据库迁移(新增表/字段)

# 本地先写 Alembic 迁移
cd backend
alembic revision --autogenerate -m "描述"

# 服务器上运行
COMPOSE="docker compose -f /home/scilit/sci-lit-manager/docker-compose.prod.yml --env-file /home/scilit/sci-lit-manager/.env"
$COMPOSE run --rm migrate

9.5 监控

# 系统资源
htop

# Docker 容器资源
docker stats

# 磁盘
df -h

10. 应急预案

10.1 服务挂了

# 查看日志
docker compose logs --tail=50 backend

# 重启
docker compose restart backend

# 如果起不来
docker compose down && docker compose up -d

10.2 数据库满了

# 清理 user_feed 旧分区
docker compose exec backend python -c "
from app.db import engine
from sqlalchemy import text
# 删除 3 个月前的 feed 分区表
"

10.3 回滚

# 如果新版本有问题
COMPOSE="docker compose -f /home/scilit/sci-lit-manager/docker-compose.prod.yml --env-file /home/scilit/sci-lit-manager/.env"

# 回滚后端
git revert <commit_hash>
$COMPOSE build backend && $COMPOSE up -d backend

10.4 紧急联系

服务 控制台
阿里云 https://ecs.console.aliyun.com/
Stripe https://dashboard.stripe.com/
域名 DNS 你的域名服务商后台
SSL 证书 certbot certificates 查看

附录 A:成本估算(小规模)

项目 月费 年费
阿里云 ECS 2C4G 40G SSD ¥100 ¥1,200
域名 oncolit.gonsun.com ¥5 ¥60
SSL (Let's Encrypt) ¥0 ¥0
MinIO 对象存储(额外磁盘) ¥0(用 ECS 磁盘) ¥0
SMTP(日<200封) ¥0 ¥0
DeepSeek(日200篇摘要,约5M tokens ~¥5 ~¥60
Stripe 交易费 按交易 2.9%
合计 ~¥110/月 ~¥1,320/年

附录 B:常用命令速查

# SSH 登录
ssh scilit@<IP>

# 进入项目目录
cd /home/scilit/sci-lit-manager

# 定义 Compose 别名
COMPOSE="docker compose -f docker-compose.prod.yml --env-file .env"

# 查看服务状态
$COMPOSE ps

# 日志
$COMPOSE logs -f backend

# 重启
$COMPOSE restart backend

# 查看所有容器资源占用
docker stats

# 数据库备份
bash /home/scilit/backup.sh

# SSL 证书续期
sudo certbot renew

# 系统更新
sudo apt update && sudo apt upgrade -y

# 磁盘/内存
df -h && free -h

11. 实际部署流程(每日代码更新)

本项目不使用 git flow 部署,采用 SCP + docker cp 直接替换文件。

11.1 如何判断是否需要重建镜像

场景 部署方式 原因
.py / .vue / .ts docker cp 文件替换即可
加/改 pip 包(requirements.txt docker compose build 容器内没有新包
加/改 npm 包(package.json docker compose build 容器内没有新包
Dockerfile / nginx.conf docker compose build 容器配置变了
改环境变量(.env docker compose up -d 不需要重建,重启读取即可

需要重建的唯一情况是:容器内需要的东西不在容器里(新依赖、新配置)。

11.2 确认 requirements.txt 是否有变更

# 拉取生产上的文件对比
ssh -i ~/.ssh/id_ed25519 root@123.207.9.209 \
  "docker exec scilit-backend-1 cat /app/requirements.txt" \
  > /tmp/requirements_prod.txt
diff d:/ClaudeCode/backend/requirements.txt /tmp/requirements_prod.txt

有输出 → 有变更,需要重建镜像。无输出 → 没变,直接 cp。

11.3 前端部署(Vue/TypeScript — 需要编译)

# 1. 构建生产包(编译 .vue/.ts → dist/
cd frontend && npm run build

# 2. 复制到服务器
ssh -i ~/.ssh/id_ed25519 root@123.207.9.209 "mkdir -p /tmp/frontend-dist"
scp -i ~/.ssh/id_ed25519 -r frontend/dist/* root@123.207.9.209:/tmp/frontend-dist/

# 3. 替换容器内文件 + 重载 nginx
ssh -i ~/.ssh/id_ed25519 root@123.207.9.209 \
  "docker cp /tmp/frontend-dist/. scilit-frontend-1:/usr/share/nginx/html/ \
   && docker exec scilit-frontend-1 nginx -s reload \
   && rm -rf /tmp/frontend-dist"

11.4 后端部署(Python — 不需要编译)

单个文件

scp -i ~/.ssh/id_ed25519 backend/app/services/xxx.py root@123.207.9.209:/tmp/
ssh -i ~/.ssh/id_ed25519 root@123.207.9.209 \
  "docker cp /tmp/xxx.py scilit-backend-1:/app/app/services/xxx.py \
   && docker restart scilit-backend-1 && rm /tmp/xxx.py"

多个文件(tar 打包一次性覆盖)

tar czf /tmp/backend_update.tar.gz -C backend app/services/ app/api/ app/schemas/
scp /tmp/backend_update.tar.gz root@123.207.9.209:/tmp/
ssh root@123.207.9.209 \
  "docker cp /tmp/backend_update.tar.gz scilit-backend-1:/tmp/ \
   && docker exec scilit-backend-1 tar xzf /tmp/backend_update.tar.gz -C /app/ \
   && docker restart scilit-backend-1"

新增依赖时(必须重建镜像)

# 在服务器上执行
cd /home/scilit/sci-lit-manager
docker compose -f docker-compose.prod.yml build backend
docker compose -f docker-compose.prod.yml up -d backend

11.5 关于 Docker 构建缓存

Dockerfile 层序决定了缓存策略:

1. FROM python:3.10          ← 缓存(镜像标签没变)
2. COPY requirements.txt     ← 缓存(文件没变)
3. RUN pip install -r ...    ← 缓存命中(requirements.txt 不变时跳过)
4. COPY app/                 ← 不命中(代码改了)
5. CMD uvicorn ...           ← 跟随上层,重建

不改 requirements.txt 时重建很快(~3-5spip install 直接跳过),只改代码没必要重建,docker cp 秒级。


12. 生产部署严格规则(2026-07-14 总结)

以下规则来自实际踩坑,每次部署前必须逐条对照。

规则 1:永远要先清理 __pycache__

# docker cp 后,务必执行:
docker exec scilit-backend-1 find /app/app -name __pycache__ -exec rm -rf {} + 2>/dev/null

否则 Python 会加载旧的 .pyc 字节码,导致神秘的 ImportError(如 cap_pub_dateis_superuser 等问题)。

规则 2:后端部署后必须同时重启 worker

docker restart scilit-backend-1
docker restart scilit-worker-1

Worker 使用独立进程,不重启则继续执行旧代码,导致定时任务(PubMed 管道、引用刷新等)行为不一致。

规则 3:前端部署必须先清理旧 assets

# 先清空旧文件,再 docker cp,否则多轮构建产物污染
ssh root@123.207.9.209 \
  "rm -rf /usr/share/nginx/html/assets /usr/share/nginx/html/index.html /usr/share/nginx/html/favicon* /usr/share/nginx/html/icon-*.png \
   && docker cp /tmp/frontend-dist/. scilit-frontend-1:/usr/share/nginx/html/ \
   && docker exec scilit-frontend-1 nginx -s reload"

不清理会导致 assets 目录混入多个构建版本的 chunk 文件(同组件 5 个不同 hash 版本),浪费磁盘且可能加载错误资源。

规则 4:部署后必须验证

# 4.1 健康检查
docker exec scilit-backend-1 curl -sf http://localhost:8000/health

# 4.2 新模块导入测试(新增文件时)
docker exec scilit-backend-1 python -c "from app.api.v1 import admin_roles, journals; print('OK')"
docker exec scilit-backend-1 python -c "from app.core import audit; print('OK')"

# 4.3 md5 抽查(确认容器与本地一致)
docker exec scilit-backend-1 md5sum /app/app/api/v1/router.py
# 与本地比对:md5sum backend/app/api/v1/router.py

# 4.4 检查是否有放错位置的文件
docker exec scilit-backend-1 ls /app/app/models/auth.py 2>/dev/null && echo "WARNING: misplaced file!" || echo "clean"

规则 5:本地新文件必须先同步到服务器项目目录

服务器项目目录 /root/scilit/ 是 Docker 镜像的构建源目录。新文件(如 models/audit.py)必须同时:

  1. 同步到服务器项目目录(scp
  2. 复制到容器(docker cp

否则容器重建后会丢失文件。

规则 6:永远不要 docker cp 到错误的目录

models/ 目录只放数据库模型类,不要放路由代码或安全工具函数。

# ❌ 错误的放法(曾经踩坑):
# 把 api/v1/auth.py → docker cp 到了 /app/app/models/auth.py

# ✅ 正确的目录对应关系:
# backend/app/api/v1/xxx.py → /app/app/api/v1/xxx.py
# backend/app/core/xxx.py  → /app/app/core/xxx.py
# backend/app/models/xxx.py → /app/app/models/xxx.py
# backend/app/services/xxx.py → /app/app/services/xxx.py
# backend/app/schemas/xxx.py → /app/app/schemas/xxx.py

规则 7:确认 pyproject.toml 无变化才用 docker cp

# 对比容器与本地 pyproject.toml
docker exec scilit-backend-1 cat /app/pyproject.toml | md5sum
md5sum backend/pyproject.toml
# 不一致 → 必须重建镜像

有变化(新增 pip 依赖)时,docker compose build backend 重建。依赖不存在于容器中会导致 ModuleNotFoundError。

规则 8:多文件部署一律用 tar 打包,不倒腾单个文件

# ✅ 正确:tar 打包整个目录
tar czf /tmp/backend_update.tar.gz -C backend app/services/ app/api/ app/core/ app/models/ app/schemas/ app/tasks/
scp /tmp/backend_update.tar.gz root@123.207.9.209:/tmp/
ssh root@123.207.9.209 \
  "docker cp /tmp/backend_update.tar.gz scilit-backend-1:/tmp/ \
   && docker exec scilit-backend-1 tar xzf /tmp/backend_update.tar.gz -C /app/ \
   && docker exec scilit-backend-1 find /app/app -name __pycache__ -exec rm -rf {} + 2>/dev/null \
   && docker restart scilit-backend-1 && docker restart scilit-worker-1"

单文件 scp 容易漏复制依赖文件,且文件较小时 tar 的开销可忽略。

规则 8b:不确定改了什么文件时,全量同步 app/

# ✅ 最安全:整个 app/ 目录打包
cd d:/ClaudeCode/backend
tar czf /tmp/backend-app.tar.gz --exclude="__pycache__" --exclude="*.pyc" app/
scp /tmp/backend-app.tar.gz root@123.207.9.209:/tmp/
ssh root@123.207.9.209 \
  "docker cp /tmp/backend-app.tar.gz scilit-backend-1:/tmp/ \
   && docker exec scilit-backend-1 tar xzf /tmp/backend-app.tar.gz -C /app/ \
   && docker exec scilit-backend-1 find /app/app -name __pycache__ -exec rm -rf {} + 2>/dev/null \
   && docker restart scilit-backend-1 && docker restart scilit-worker-1"

为什么要全量? 模型文件(models/literature.py 等)可能被多个路由和 services 引用。只 cp 改了的 API 文件而漏掉模型更新,启动后出现 AttributeError: type object 'GlobalTag' has no attribute 'source''User' object has no attribute 'admin_note',症状是后台大面积请求失败。

何时用:

  • 改的是 models/schemas/ 下的文件 → 全量
  • 改的是 core/ 下的基础设施类 → 全量
  • 只改单个 API 路由(如 admin.py),无公共依赖 → 单个 tar 可接受
  • 不确改了什么 → 全量,10s 的事比诊断 AttributeError 快

规则 9:服务器不是 git 仓库,手动同步代替 git pull

服务器 /root/scilit/ 不是 git 仓库。同步方式:

# 方式 A:全量 tar 同步
tar czf /tmp/scilit_sync.tar.gz --exclude=node_modules --exclude='__pycache__' --exclude=.git backend/ frontend/ docker-compose* .env.example
scp /tmp/scilit_sync.tar.gz root@123.207.9.209:/root/
ssh root@123.207.9.209 "cd /root/scilit && tar xzf /tmp/scilit_sync.tar.gz"

# 方式 B:仅增量同步后端 Python 文件
# 由 Claude 自动完成,使用 tar + scp + docker cp 流程

规则 10:容器内文件用 scilit 用户身份运行

Dockerfile 中有 USER scilit,容器内 Python 进程以 scilit 用户运行。docker cp 复制进去的文件属主为 root,但只要文件对 scilit 可读(-rw-r--r--),Python 就能正常使用。如果遇到权限问题:

docker exec scilit-backend-1 chown -R scilit:scilit /app/app

规则 11:新增 Alembic 迁移时,迁移文件必须在容器内

# 本地生成迁移后:
scp -r backend/alembic/versions/xxx.py root@123.207.9.209:/tmp/
ssh root@123.207.9.209 \
  "docker cp /tmp/xxx.py scilit-backend-1:/app/alembic/versions/xxx.py \
   && docker exec scilit-backend-1 alembic -c alembic/alembic.ini upgrade head"

没有迁移文件在容器内,alembic upgrade head 会报 Target database is not up to date


13. 事故记录:2026-07-17 部署回退问题复盘

背景

一次正常的安全加固部署(CSP 统一、Refresh token 告警、验证码集成、JWT 校验、注册限速、Nginx 日志持久化),先后出现多个问题,反复修复。

问题链(按时间顺序)

问题 1docker build cache 被清除 → 25min 重建

现象: docker compose build backend 重新下载所有 pip 包,构建超 25 分钟。

根因: 上一轮部署执行了 docker builder prune -a -f,所有构建缓存层被清除。

教训: 不改 requirements.txt 时,pip install 是缓存的,构建只要几秒。永远不要在生产服务器上 prune -a。如需清理磁盘空间,指定 docker builder prune(不加 -a,只清 dangling 层)。

正确做法: 不能构建时用 docker cp 热更新,不需要构建。能构建时用缓存,几秒完事。


问题 2tar 路径前缀嵌套

现象: tar xzf backend.tar.gz -C /root/scilit/backend/ 后,文件到了 /root/scilit/backend/backend/app/...

根因: tar 包内的路径前缀是 backend/tar czf backend.tar.gz backend/app/),而解压目标已经是 backend/,导致嵌套。

教训: 打包时用 --strip-components=1,或直接在 backend/ 目录内打包。

正确做法:

# 方式 A:在子目录打包
cd backend && tar czf /tmp/backend-app.tar.gz app/

# 方式 Bstrip-components
tar czf /tmp/backend-app.tar.gz -C backend app/
ssh ... "tar xzf /tmp/backend-app.tar.gz --strip-components=1 -C /root/scilit/backend/"

问题 3Backend 容器和 Postgres 在不同 Docker 网络

现象: backend 容器启动后 Health check DB connection failedalembic upgrade headName or service not known

根因: compose 文件定义了两个 networkscilitscilit_default)。postgres 在 scilit_defaultbackend 在 scilit_scilit

教训: 生产 compose 文件定义了 networks: scilit,但 postgres 服务没有指定 networks:Docker 自动为其创建默认的 scilit_default 网络。该问题在容器重启后反复出现。

正确做法: 所有服务统一在同一网络。compose 文件中每个服务都显式指定 networks: - scilit(已修复)。


问题 4Nginx 502 Bad GatewayBackend 容器 IP 变化)

现象: backend 容器重启后,nginx 代理报 502。直接访问 backend127.0.0.1:8000)正常。

根因: Nginx proxy_pass http://backend:8000 使用固定字符串解析,只在启动时解析一次 DNS。Backend 容器重启后 Docker 为它分配了新 IP,nginx 仍用旧 IP 连接。

教训: Docker 环境中,nginx 代理上游必须用变量形式 + resolver 指令触发动态 DNS 解析。

正确做法:

resolver 127.0.0.11 ipv6=off valid=30s;
server {
    set $backend_upstream http://backend:8000;
    location /api/ {
        proxy_pass $backend_upstream;
    }
}

问题 5docker compose up -d --no-deps frontend 覆盖了 docker cp 的文件

现象: 前端标签显示空白。docker cp 进去的新前端文件(RegisterView 等)消失了。

根因: 修改 compose 文件(加日志 volume)后执行 docker compose up -d --no-deps frontendDocker 重建了容器,从旧镜像启动。之前 docker cp 进去的文件不在镜像中,所以全部丢失。

教训: docker cp 修改的是运行中的容器文件系统,不是镜像。容器重建后镜像内容胜出。容器不重建时 cp 永久有效。

正确做法:

# 方案一:cp 后立即保存为新镜像(最简单)
docker cp dist/. frontend:/usr/share/nginx/html/
docker commit scilit-frontend-1 scilit-frontend:latest

# 方案二:改 compose 前先构建新镜像
docker compose build frontend
# 再改 compose 文件 → up -d

# 方案三:不改 compose,只在运行中容器 cp

根本原因

全部问题有一个共同模式:每次部署依赖"上下文常识",而不是依赖流程。 具体来说:

  1. 不知道 proxy_pass 固定字符串会缓存 DNS → nginx 502
  2. 不知道 docker compose up -d 会重建容器 → cp 的内容丢失
  3. 不知道 prune -a 会清 pip 缓存 → 多等 25 分钟
  4. 不知道 tar 路径前缀 → 文件放到错误目录

每个问题单独看都是 Docker 基础常识,但在多步骤部署中,每步引入一个"我不知道这里还有这个坑"的盲点,累积后大面积出错。

改进点

问题 预防措施 状态
构建缓存被清 不执行 docker builder prune -a 已记录 memory
tar 嵌套 统一用 --strip-components=1 或先 cd 再打包 规则 8 已更新
网络不一致 所有服务显式指定 networks compose 已修
nginx 502 变量形式 proxy_pass + resolver nginx.conf 已修
docker cp 被覆盖 改 compose 前先 build,或 cp 后立即 commit 规则 12 见下

规则 12:前端 docker cp 后立即 commit 镜像

# 前端部署后必须保存为新镜像
docker commit scilit-frontend-1 scilit-frontend:latest

否则任何导致容器重建的操作(docker compose up -d、改 compose 文件、服务器重启后 compose up)都会丢失 cp 的文件。后端容器(scilit-backend-1)很少重建,不需要 commit,但前端容器需要。

规则 13:改 compose 文件前先构建镜像

# 先确保镜像包含最新代码
docker compose build frontend   # 前端(npm,几秒)
# 或
docker compose build backend    # 后端(pip 缓存命中时几秒)

# 再改 compose 配置 → up -d 重建容器

docker-compose.prod.yml 必然触发容器重建,镜像必须包含最新代码。