# SciLit 生产部署实际操作记录

**部署日期：** 2026-07-10 ~ 2026-07-11
**服务器：** 123.207.9.209（腾讯云 · TencentOS 4）
**部署方式：** Docker Compose（Nginx 前端反代 + Uvicorn 后端 API）

---

## 1. 服务器环境

### 1.1 基础信息

- **OS：** TencentOS 4（RHEL 系，基于 RHEL 9，非 Ubuntu）
- **磁盘：** 20GB（/dev/vda1），已用 ~9GB（部署含镜像后 ~12GB）
- **CPU/内存：** 未确认，当前 Elasticsearch 分配 1GB，可跑完整 Docker 栈

### 1.2 Docker 安装（非标准流程）

TencentOS 4 不被 Docker 官方安装脚本支持（`get.docker.com` 会报不支持的发行版）。

```bash
# 不能用标准方法：
# curl -fsSL https://get.docker.com | bash  # ❌ 失败

# 改用 Tencent 源 + --releasever=9（兼容 CentOS 9）
dnf install -y dnf-plugins-core
dnf config-manager --add-repo https://mirrors.cloud.tencent.com/docker-ce/linux/rhel/docker-ce.repo
dnf --releasever=9 install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
systemctl enable --now docker
```

### 1.3 Docker 镜像加速配置

国内拉取 `docker.io` 镜像极慢或超时，必须配置镜像加速器：

```json
# /etc/docker/daemon.json
{
  "registry-mirrors": [
    "https://mirror.ccs.tencentyun.com",
    "https://docker.mirrors.ustc.edu.cn"
  ]
}
```

配置后重启 Docker：
```bash
systemctl daemon-reexec && systemctl restart docker
```

### 1.4 验证安装

```bash
docker info                            # Registry Mirrors 应显示加速地址
docker run hello-world                 # 拉取测试
docker compose version                  # 应显示 v2.x
```

---

## 2. SSH 连接

### 2.1 服务器 SSH 配置

服务器 SSH 默认**禁用密码登录 + 禁用 root 登录**（`PasswordAuthentication no`, `PermitRootLogin no`）。

需要在腾讯云控制台 VNC 或 web shell 中修改：
```bash
# /etc/ssh/sshd_config
PermitRootLogin yes
PasswordAuthentication yes
systemctl restart sshd
```

### 2.2 本地 SSH 工具

- **Git Bash 的 `ssh`**：密码登录交互模式在自动化场景不可靠
- **`sshpass`**：Windows Git Bash 不支持（exit code 127）
- **`pexpect`**：Windows 下 import 失败
- **`plink`（PuTTY）**：不支持 OpenSSH 新版私钥格式（`BEGIN OPENSSH PRIVATE KEY`）
- **解决方案**：在服务器添加本地生成的公钥到 `~/.ssh/authorized_keys`，用密钥登录

```bash
# 本地生成密钥
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N "" -C "deploy"

# 将公钥内容添加到服务器 ~/.ssh/authorized_keys
echo 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... claude-code-deploy' >> ~/.ssh/authorized_keys

# 登录
ssh -i ~/.ssh/id_ed25519 root@123.207.9.209
```

---

## 3. 代码传输与部署目录

### 3.1 传输方式

用 SCP（无 rsync）：

```bash
scp -i ~/.ssh/id_ed25519 -r d:/ClaudeCode/* root@123.207.9.209:/root/scilit/
```

注意：
- SCP 不支持 `--exclude`（要用 rsync），大目录（node_modules、.git）需要手动排除
- 传输前清理无关文件可大幅减少时间
- SCP 不会覆盖已存在的同名文件（除非加 `-r` 强制）

### 3.2 部署目录

```
/root/scilit/
├── docker-compose.prod.yml     # 生产 compose 配置
├── .env                        # 环境变量（含敏感信息）
├── backend/                    # 后端代码 + Dockerfile
└── frontend/                   # 前端代码 + Dockerfile.prod
```

---

## 4. Dockerfile 构建（关键问题和修复）

### 4.1 `elasticsearch-py` 包名错误

**问题：** `pyproject.toml` 中写的是 `elasticsearch-py`，这是旧版/别名。PyPI 上的正确包名是 `elasticsearch`。

```bash
# 报错信息
ERROR: Could not find a version that satisfies the requirement elasticsearch-py>=8.14.0
```

**修复：** `pyproject.toml` 中
```diff
- "elasticsearch-py[async]>=8.14.0",
+ "elasticsearch[async]>=8.14.0",
```

同时添加 `email-validator` 依赖（Pydantic 验证 email 字段需要）：
```diff
+ "email-validator>=2.0",
```

### 4.2 PyPI 镜像导致包缺失

**问题：** 腾讯 PyPI 镜像（`mirrors.cloud.tencent.com/pypi/simple/`）缺少 `elasticsearch>=8.14.0`。

**解决：** 分两步预装

```dockerfile
COPY pyproject.toml .
# 第一步：腾讯镜像有 elasticsearch 就用，没有会报错
RUN pip install --no-cache-dir -i https://mirrors.cloud.tencent.com/pypi/simple/ "elasticsearch[async]>=8.14.0" "email-validator>=2.0"
# 第二步：腾讯镜像有其余包（fastapi, sqlalchemy, etc.）
RUN pip install --no-cache-dir -i https://mirrors.cloud.tencent.com/pypi/simple/ .
```

实测结果：
- 4/4/2026 时腾讯镜像**有** `elasticsearch>=8.14.0`（已验证通过）
- 如果未来腾讯镜像再次缺失，改为两步：官方 PyPI 先装 elasticsearch，腾讯镜像装其余包

### 4.3 Debian apt 源

Docker 基础镜像 `python:3.12-slim` 基于 Debian。腾讯 apt 源地址：
```
http://mirrors.tencent.com/debian          → trixie main/updates
http://mirrors.tencent.com/debian-security → trixie-security main
```

Dockerfile 中：
```dockerfile
RUN echo "deb http://mirrors.tencent.com/debian trixie main" > /etc/apt/sources.list \
 && echo "deb http://mirrors.tencent.com/debian trixie-updates main" >> /etc/apt/sources.list \
 && echo "deb http://mirrors.tencent.com/debian-security trixie-security main" >> /etc/apt/sources.list \
 && apt-get update \
 && apt-get install -y --no-install-recommends \
    build-essential libpq-dev \
 && rm -rf /var/lib/apt/lists/*
```

注意 `deb.debian.org` 在中国大陆极慢（~100 KB/s），覆盖 sources.list 后**需要确认** `apt-get update` 的输出只从 `mirrors.tencent.com` 获取。如果尚有 `deb.debian.org` 的记录，是 docker build cache 导致，加 `--no-cache` 或使用 `COPY --chmod` 在早期层清掉默认配置。

### 4.4 `alembic/env.py` 找不到 app 模块

**问题：** `alembic env.py` 中 `from app.config import settings` 在工作目录（`/app/alembic/`）下找不到同级 `app/` 目录。

**修复：** 在 env.py 顶部添加 sys.path：

```python
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
```

同时 `docker-compose.prod.yml` 中 `migrate` 服务需要添加 `JWT_SECRET`、`SPECIALTY` 等环境变量（因为 `app.config.Settings()` 验证配置时会检查这些值）：

```yaml
migrate:
  environment:
    DATABASE_URL: ...
    JWT_SECRET: ${JWT_SECRET}
    SPECIALTY: ${SPECIALTY:-oncology}
    DEBUG: "false"
```

### 4.5 Docker 构建速度优化

| 优化前 | 优化后 |
|-------|--------|
| 单层 `pip install .`，全部从官方 PyPI 下载，~100 KB/s | 分两步：腾讯镜像加速下载（~50 MB/s），全量约 15s |
| 每次修改代码需全部重建 | `COPY pyproject.toml .` + 依赖安装缓存 → 仅改代码时跳过 pip install |

关键思路：**将依赖安装与应用代码分离**，只在 `pyproject.toml` 变更时重跑 pip：

```dockerfile
# 先拷贝仅 pyproject.toml → 安装依赖（缓存层）
COPY pyproject.toml .
RUN pip install --no-cache-dir ...
# 最后拷贝全部代码（变更频繁，但不会触发依赖重装）
COPY . .
```

---

## 5. Docker Compose 启动流程

### 5.1 命令

```bash
cd /root/scilit
docker compose -f docker-compose.prod.yml --env-file .env up -d
```

### 5.2 容器启动顺序（depends_on 控制）

```
postgres ─┬→ migrate ─→ backend ─→ frontend
redis ────┤              │
es ───────┤              └→ worker
minio ────┘
```

- `postgres` 必须 health check 通过 → `migrate` 才启动（运行 alembic upgrade）
- `migrate` exit 0 → `backend` + `worker` 才启动
- `backend` 健康检查通过 → `frontend` 才启动（虽然 frontend 只是 Nginx 静态文件，无实质依赖）

### 5.3 迁移日志确认

启动后检查：
```bash
docker logs scilit-migrate-1    # 确认所有 migration 版本 applied
docker ps --format 'table {{.Names}}\t{{.Status}}'  # 确认全部 healthy
```

### 5.4 健康检查

```bash
# 后端 API
curl http://localhost:8000/health
# → {"status":"ok","specialty":"oncology","db":"ok","version":"0.1.0"}

# 前端
curl http://localhost/
# → <!DOCTYPE html><html lang="zh-CN">...

# 外网
curl http://123.207.9.209/health
```

---

## 6. 本次部署的变更

### 6.1 代码变更

| 文件 | 变更内容 |
|------|---------|
| `docker-compose.prod.yml` | migrate 服务添加 JWT_SECRET/SPECIALTY/DEBUG 环境变量；移除已废弃的`version`属性；添加 COS env vars；新增 `statusline-setup` services 等 |
| `backend/Dockerfile` | 腾讯镜像源覆盖默认 deb.debian.org；两步 pip 策略预装 elasticsearch |
| `backend/pyproject.toml` | `elasticsearch-py` → `elasticsearch`；新增 `email-validator` |
| `backend/alembic/env.py` | 添加 `sys.path.insert(0, ...)` 解决模块导入 |
| `backend/app/models/literature.py` | 新增 `full_text_path` 字段 |
| 新增 `backend/app/services/cos_client.py` | 腾讯云 COS 客户端 |
| 新增 `alembic/versions/d959d4b8a9f1_*.py` | 添加 full_text_path 列迁移 |

### 6.2 基础设施变更

| 项目 | 之前 | 现在 |
|------|------|------|
| 对象存储 | MinIO（自建） | 腾讯云 COS（生产，可降级到 MinIO） |
| Dockerfile apt 源 | deb.debian.org（极慢） | mirrors.tencent.com（~50 MB/s） |
| Dockerfile pip 源 | 无/官方 PyPI | mirrors.cloud.tencent.com（腾讯镜像加速） |
| SSH 认证 | 密码 | SSH Key + 密码均可 |

---

## 7. 回滚要点

- Docker image tag 未设置版本号，`scilit-backend:latest` 覆盖上次构建。如需回滚需重建旧代码
- 数据库迁移是递增的，回滚代码后需确认兼容性。如须回退迁移：`alembic downgrade -1`
- COS 迁移不需回滚：`full_text_sections` 字段仍保留，COS 不可用时自动降级到 PG
- `.env` 文件包含敏感信息，如需回滚注意不要覆盖

---

## 8. 注意事项

1. **不要用 `docker compose down -v`**：`-v` 会删除所有数据卷（包括 PG 数据库、ES 索引、Redis 数据、MinIO 文件）
2. **env 文件必须安全存放**：`.env` 包含 SMTP 密码、JWT 密钥、DeepSeek API Key、PubMed API Key
3. **Elasticsearch 吃内存**：生产已配置 `-Xms1g -Xmx1g`。如果内存紧张可以暂不启动 ES（代码中 `ES_URL` 为空时自动降级到 tsvector）
4. **Docker 日志**：每个容器配置 `logging.driver: json-file` + `max-size: 10m`, `max-file: 3`，防止日志占满磁盘
5. **Nginx 端口 80** 映射到前端容器，通过 iptables/防火墙控制。SSL/443 暂未配置，需上游反代（Caddy/Nginx/CLB）
