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.
39 KiB
SciLit Oncology — 生产部署文档
目录
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 }
小规模起步可以先不启 ES(
ES_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 控制台端口为 9001,API 端口为 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.yml 中 frontend 服务使用 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 API(DeepSeek,已配置)
已在 `.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_USER和MINIO_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 API,3 并发)
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-7天)
- 认证费用 ¥300/年
- 认证后在"开发 → 基本配置"获取 AppID + AppSecret
- 在"功能 → 模板消息"申请模板(审核 1-3 天)
- 在项目中配置:
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 之间添加 PgBouncer(transaction 模式)。届时需下调 pool_size 避免连接堆积。
8.6 首页 Feed 缓存
首页文献列表使用预缓存策略(类似热搜缓存 hot_articles_cache.py)。
| 项 | 值 |
|---|---|
| 缓存 Key | homepage:feed |
| TTL | 1800s(30 分钟) |
| 刷新任务 | refresh_homepage_feed,ARQ 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-5s,pip 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_date、is_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)必须同时:
- 同步到服务器项目目录(scp)
- 复制到容器(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 日志持久化),先后出现多个问题,反复修复。
问题链(按时间顺序)
问题 1:docker 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 热更新,不需要构建。能构建时用缓存,几秒完事。
问题 2:tar 路径前缀嵌套
现象: 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/
# 方式 B:strip-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/"
问题 3:Backend 容器和 Postgres 在不同 Docker 网络
现象: backend 容器启动后 Health check DB connection failed,alembic upgrade head 报 Name or service not known。
根因: compose 文件定义了两个 network(scilit 和 scilit_default)。postgres 在 scilit_default,backend 在 scilit_scilit。
教训: 生产 compose 文件定义了 networks: scilit,但 postgres 服务没有指定 networks:,Docker 自动为其创建默认的 scilit_default 网络。该问题在容器重启后反复出现。
正确做法: 所有服务统一在同一网络。compose 文件中每个服务都显式指定 networks: - scilit(已修复)。
问题 4:Nginx 502 Bad Gateway(Backend 容器 IP 变化)
现象: backend 容器重启后,nginx 代理报 502。直接访问 backend(127.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;
}
}
问题 5:docker compose up -d --no-deps frontend 覆盖了 docker cp 的文件
现象: 前端标签显示空白。docker cp 进去的新前端文件(RegisterView 等)消失了。
根因: 修改 compose 文件(加日志 volume)后执行 docker compose up -d --no-deps frontend,Docker 重建了容器,从旧镜像启动。之前 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
根本原因
全部问题有一个共同模式:每次部署依赖"上下文常识",而不是依赖流程。 具体来说:
- 不知道
proxy_pass固定字符串会缓存 DNS → nginx 502 - 不知道
docker compose up -d会重建容器 → cp 的内容丢失 - 不知道 prune -a 会清 pip 缓存 → 多等 25 分钟
- 不知道 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 必然触发容器重建,镜像必须包含最新代码。