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.
1245 lines
39 KiB
Markdown
1245 lines
39 KiB
Markdown
# SciLit Oncology — 生产部署文档
|
||
|
||
## 目录
|
||
|
||
1. [前置准备](#1-前置准备)
|
||
2. [服务器基础环境](#2-服务器基础环境)
|
||
3. [中间件安装](#3-中间件安装)
|
||
4. [域名与SSL](#4-域名与ssl)
|
||
5. [应用部署](#5-应用部署)
|
||
6. [数据库初始化](#6-数据库初始化)
|
||
7. [第三方服务注册](#7-第三方服务注册)
|
||
8. [上线验证清单](#8-上线验证清单)
|
||
9. [日常运维](#9-日常运维)
|
||
10. [应急预案](#10-应急预案)
|
||
|
||
---
|
||
|
||
## 1. 前置准备
|
||
|
||
### 1.1 你需要准备好的东西
|
||
|
||
| 项目 | 说明 | 在哪里获取 |
|
||
|------|------|----------|
|
||
| ☁️ 服务器 | Linux (Ubuntu 22.04 推荐), 2C4G 起步,50G SSD | 阿里云/腾讯云/AWS |
|
||
| 🔑 SSH 密钥 | 用于免密登录服务器 | `ssh-keygen` 本地生成,公钥上传到云控制台 |
|
||
| 🌐 域名 | `oncolit.gonsun.com` 或你的域名 | 阿里云/腾讯云/Namecheap |
|
||
| 📧 邮箱 | 用于 SMTP 发件和系统通知 | 阿里云邮件推送 / SendGrid |
|
||
|
||
### 1.2 本地先确认项目能跑
|
||
|
||
```bash
|
||
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 登录服务器
|
||
|
||
```bash
|
||
ssh root@<你的服务器IP>
|
||
|
||
# 创建非 root 用户
|
||
adduser scilit
|
||
usermod -aG sudo scilit
|
||
su - scilit
|
||
```
|
||
|
||
### 2.2 更新系统 + 安装基础工具
|
||
|
||
```bash
|
||
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
|
||
|
||
```bash
|
||
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)
|
||
|
||
```bash
|
||
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 中已定义,无需额外操作:
|
||
|
||
```yaml
|
||
# 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(自建)
|
||
|
||
```yaml
|
||
redis:
|
||
image: redis:7-alpine
|
||
command: redis-server --appendonly yes # 开启持久化
|
||
volumes:
|
||
- /data/redis:/data
|
||
restart: unless-stopped
|
||
```
|
||
|
||
### 3.4 Elasticsearch(可选,搜索增强)
|
||
|
||
```yaml
|
||
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 原文。
|
||
|
||
```yaml
|
||
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。
|
||
|
||
```bash
|
||
# docker-compose.prod.yml 启动后,frontend 容器监听 80 端口
|
||
# 宿主机只需安装 Nginx 做 SSL 终止和域名转发:
|
||
sudo apt install -y nginx certbot python3-certbot-nginx
|
||
```
|
||
|
||
```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 直接托管前端文件**
|
||
|
||
```bash
|
||
sudo vim /etc/nginx/sites-available/scilit-oncology
|
||
```
|
||
|
||
```nginx
|
||
# /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";
|
||
}
|
||
}
|
||
```
|
||
|
||
```bash
|
||
# 启用站点
|
||
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 免费)
|
||
|
||
```bash
|
||
sudo certbot --nginx -d oncolit.gonsun.com -d www.oncolit.gonsun.com
|
||
# 按提示输入邮箱,同意条款
|
||
|
||
# 设置自动续期
|
||
sudo certbot renew --dry-run
|
||
```
|
||
|
||
---
|
||
|
||
## 5. 应用部署
|
||
|
||
### 5.1 上传项目
|
||
|
||
```bash
|
||
# 本地打包(排除 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 创建生产环境变量
|
||
|
||
```bash
|
||
# /home/scilit/sci-lit-manager/.env
|
||
vim .env
|
||
```
|
||
|
||
```bash
|
||
# ===== 数据库(必须修改) =====
|
||
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 镜像并启动
|
||
|
||
```bash
|
||
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 验证服务
|
||
|
||
```bash
|
||
# 健康检查
|
||
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。首次初始化:
|
||
|
||
```bash
|
||
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 数据备份
|
||
|
||
```bash
|
||
# 每日备份脚本
|
||
# /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"
|
||
```
|
||
|
||
```bash
|
||
# 添加定时任务(每天凌晨 3 点备份)
|
||
crontab -e
|
||
# 添加:
|
||
0 3 * * * /home/scilit/backup.sh >> /home/scilit/backups/backup.log 2>&1
|
||
```
|
||
|
||
### 6.3 PubMed 基线数据导入
|
||
|
||
```bash
|
||
# 下载 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 密码。验证方式:
|
||
|
||
```bash
|
||
# 发送测试邮件
|
||
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 服务。首次部署后操作:
|
||
|
||
```bash
|
||
# 访问 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:
|
||
|
||
```bash
|
||
PUBMED_API_KEY=692e319f33129b7a5d2d94c57b67739e0a08
|
||
```
|
||
|
||
如 Key 过期或被轮换,登录 [NCBI Account](https://account.ncbi.nlm.nih.gov/settings/) → API Key Management 更新。
|
||
|
||
#### 7.5.2 部署后一次性数据填充
|
||
|
||
按以下顺序执行(越靠前的越关键):
|
||
|
||
```bash
|
||
# 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. 注册 [微信公众平台](https://mp.weixin.qq.com/)
|
||
2. 选择"服务号" → 提交营业执照 → 等待审核(1-7天)
|
||
3. 认证费用 ¥300/年
|
||
4. 认证后在"开发 → 基本配置"获取 AppID + AppSecret
|
||
5. 在"功能 → 模板消息"申请模板(审核 1-3 天)
|
||
6. 在项目中配置:
|
||
```bash
|
||
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](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 查看日志
|
||
|
||
```bash
|
||
# 所有服务(使用生产 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 重启服务
|
||
|
||
```bash
|
||
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 更新代码
|
||
|
||
```bash
|
||
# 服务器
|
||
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 数据库迁移(新增表/字段)
|
||
|
||
```bash
|
||
# 本地先写 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 监控
|
||
|
||
```bash
|
||
# 系统资源
|
||
htop
|
||
|
||
# Docker 容器资源
|
||
docker stats
|
||
|
||
# 磁盘
|
||
df -h
|
||
```
|
||
|
||
---
|
||
|
||
## 10. 应急预案
|
||
|
||
### 10.1 服务挂了
|
||
|
||
```bash
|
||
# 查看日志
|
||
docker compose logs --tail=50 backend
|
||
|
||
# 重启
|
||
docker compose restart backend
|
||
|
||
# 如果起不来
|
||
docker compose down && docker compose up -d
|
||
```
|
||
|
||
### 10.2 数据库满了
|
||
|
||
```bash
|
||
# 清理 user_feed 旧分区
|
||
docker compose exec backend python -c "
|
||
from app.db import engine
|
||
from sqlalchemy import text
|
||
# 删除 3 个月前的 feed 分区表
|
||
"
|
||
```
|
||
|
||
### 10.3 回滚
|
||
|
||
```bash
|
||
# 如果新版本有问题
|
||
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:常用命令速查
|
||
|
||
```bash
|
||
# 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 是否有变更
|
||
|
||
```bash
|
||
# 拉取生产上的文件对比
|
||
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 — 需要编译)
|
||
|
||
```bash
|
||
# 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 — 不需要编译)
|
||
|
||
#### 单个文件
|
||
|
||
```bash
|
||
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 打包一次性覆盖)
|
||
|
||
```bash
|
||
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"
|
||
```
|
||
|
||
#### 新增依赖时(必须重建镜像)
|
||
|
||
```bash
|
||
# 在服务器上执行
|
||
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__`
|
||
|
||
```bash
|
||
# 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
|
||
|
||
```bash
|
||
docker restart scilit-backend-1
|
||
docker restart scilit-worker-1
|
||
```
|
||
|
||
Worker 使用独立进程,不重启则继续执行旧代码,导致定时任务(PubMed 管道、引用刷新等)行为不一致。
|
||
|
||
### 规则 3:前端部署必须先清理旧 assets
|
||
|
||
```bash
|
||
# 先清空旧文件,再 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:部署后必须验证
|
||
|
||
```bash
|
||
# 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/` 目录只放数据库模型类,不要放路由代码或安全工具函数。
|
||
|
||
```bash
|
||
# ❌ 错误的放法(曾经踩坑):
|
||
# 把 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
|
||
|
||
```bash
|
||
# 对比容器与本地 pyproject.toml
|
||
docker exec scilit-backend-1 cat /app/pyproject.toml | md5sum
|
||
md5sum backend/pyproject.toml
|
||
# 不一致 → 必须重建镜像
|
||
```
|
||
|
||
有变化(新增 pip 依赖)时,`docker compose build backend` 重建。依赖不存在于容器中会导致 ModuleNotFoundError。
|
||
|
||
### 规则 8:多文件部署一律用 tar 打包,不倒腾单个文件
|
||
|
||
```bash
|
||
# ✅ 正确: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/`
|
||
|
||
```bash
|
||
# ✅ 最安全:整个 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 仓库。同步方式:
|
||
|
||
```bash
|
||
# 方式 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 就能正常使用。如果遇到权限问题:
|
||
|
||
```bash
|
||
docker exec scilit-backend-1 chown -R scilit:scilit /app/app
|
||
```
|
||
|
||
### 规则 11:新增 Alembic 迁移时,迁移文件必须在容器内
|
||
|
||
```bash
|
||
# 本地生成迁移后:
|
||
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/` 目录内打包。
|
||
|
||
**正确做法:**
|
||
```bash
|
||
# 方式 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 解析。
|
||
|
||
**正确做法:**
|
||
```nginx
|
||
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 永久有效。
|
||
|
||
**正确做法:**
|
||
```bash
|
||
# 方案一: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 镜像
|
||
|
||
```bash
|
||
# 前端部署后必须保存为新镜像
|
||
docker commit scilit-frontend-1 scilit-frontend:latest
|
||
```
|
||
|
||
否则任何导致容器重建的操作(`docker compose up -d`、改 compose 文件、服务器重启后 `compose up`)都会丢失 cp 的文件。后端容器(`scilit-backend-1`)很少重建,不需要 commit,但前端容器需要。
|
||
|
||
### 规则 13:改 compose 文件前先构建镜像
|
||
|
||
```bash
|
||
# 先确保镜像包含最新代码
|
||
docker compose build frontend # 前端(npm,几秒)
|
||
# 或
|
||
docker compose build backend # 后端(pip 缓存命中时几秒)
|
||
|
||
# 再改 compose 配置 → up -d 重建容器
|
||
```
|
||
|
||
改 `docker-compose.prod.yml` 必然触发容器重建,镜像必须包含最新代码。
|