# 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@:/home/scilit/ # 服务器端解压 ssh scilit@ 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', '

OK

')) " ### 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 $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@ # 进入项目目录 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` 必然触发容器重建,镜像必须包含最新代码。