# IT 智能服务台 — 部署架构重构方案(镜像烘焙 → 代码卷挂载)
> **文档编号**: ARCH-VOLMOUNT-001
> **编写人**: 架构师 高见远
> **日期**: 2025-07-10
> **目标服务器**: 10.90.5.110 (itsupport.servyou.com.cn)
> **方案编号**: 方案 C — 代码卷挂载替代镜像烘焙
---
## 1. 架构变更说明
### 1.1 当前架构问题(镜像烘焙模式)
当前后端代码通过 Dockerfile 的 `COPY . .` 指令在 **构建时** 烘焙进镜像。这一模式存在以下根本性缺陷:
| 问题 | 描述 | 后果 |
|------|------|------|
| **双代码源** | 服务器上存在两份代码:`/opt/wecom-it-desk/app/`(较新,部署用)+ `/opt/wecom-it-desk/backend/app/`(较旧,构建用) | 构建出的镜像可能包含过期/缺失代码 |
| **构建上下文陷阱** | `docker compose build` 的 context 为 `./backend`,`COPY . .` 复制的是 `backend/app/`(旧代码),而非 `app/`(新代码) | 07-07 和 07-10 两次认证故障的根因 |
| **重建即风险** | 每次代码更新都必须 `docker compose build --no-cache`,重建过程耗时 3-5 分钟,期间服务不可用 | 部署窗口长,故障概率高 |
| **代码与依赖耦合** | 代码和 Python 依赖包混在同一镜像层,更新代码也必须重建依赖层 | 浪费时间,增加出错面 |
| **回滚困难** | 回滚需要重新构建镜像,无法快速切换到上一版本 | 故障恢复时间长 |
**事故复盘**:
- **07-07**:Docker 镜像未重新构建 → 路由缺失(旧镜像运行新代码期望)
- **07-10**:镜像从 `backend/app/`(旧代码)构建 → 缺少 `auth.py` 等文件 → 认证全断
### 1.2 目标架构设计(卷挂载模式)
```
┌─────────────────────────────────────────────────────────┐
│ 宿主机 /opt/wecom-it-desk/ │
│ │
│ ┌──────────────┐ ┌──────────────────────────────┐ │
│ │ backend/ │ │ app/ │ │
│ │ ├ Dockerfile│ │ ├ main.py │ │
│ │ ├ req...txt │ │ ├ auth.py │ │
│ │ └ (无app/) │ │ ├ api/ │ │
│ │ (仅构建配置) │ │ ├ services/ │ │
│ │ │ │ └ ... │ │
│ └──────┬───────┘ └──────────┬────────────────────┘ │
│ │ │ volume mount │
│ │ build context │ ./app → /app/app │
│ │ (仅 requirements) │ │
└─────────┼──────────────────────┼─────────────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────┐
│ Docker Container (wecom_it_backend) │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ /app/ │ │
│ │ ├ app/ ← 卷挂载(运行时读取宿主机代码) │ │
│ │ │ ├ main.py │ │
│ │ │ └ ... │ │
│ │ ├ uploads/ ← Docker named volume │ │
│ │ ├ logs/ ← 宿主机目录挂载 │ │
│ │ └ site-packages/ ← 镜像内置(构建时安装) │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ 镜像 = Python 运行时 + 依赖包(无业务代码) │
└─────────────────────────────────────────────────────────┘
```
**核心原理**:
- **镜像** 只包含 Python 运行时 + `site-packages`(依赖包),不含业务代码
- **代码** 通过 Docker volume 挂载到容器 `/app/app/`,运行时从宿主机实时读取
- **代码更新** = 更新宿主机文件 + 重启容器(秒级,无需重建镜像)
- **依赖更新** = 更新 `requirements.txt` + 重建镜像(仅在依赖变化时)
### 1.3 核心变更点
| # | 变更项 | 变更前 | 变更后 |
|---|--------|--------|--------|
| 1 | Dockerfile `COPY . .` | 代码烘焙进镜像 | **删除**,代码通过 volume 提供 |
| 2 | Dockerfile `ENV` | 无 | 新增 `PYTHONDONTWRITEBYTECODE=1` |
| 3 | docker-compose.yml `volumes` | 仅 uploads + logs | 新增 `./app:/app/app` 代码挂载 |
| 4 | 服务器目录 `backend/app/` | 旧代码(构建用) | **删除**(不再需要) |
| 5 | 服务器目录 `app/` | 新代码(部署用) | **唯一代码源**(volume 挂载源) |
| 6 | 部署流程 | 每次重建镜像 | 代码更新仅重启,依赖更新才重建 |
---
## 2. docker-compose.yml 变更
### 2.1 变更对比
**变更前**(backend 服务 volumes 部分):
```yaml
volumes:
- backend-uploads:/app/uploads
- ${RUNTIME_LOG_HOST_DIR:-/var/log/wecom-it-desk}:/app/logs
```
**变更后**:
```yaml
volumes:
- ./app:/app/app # [新增] 代码卷挂载
- backend-uploads:/app/uploads
- ${RUNTIME_LOG_HOST_DIR:-/var/log/wecom-it-desk}:/app/logs
```
### 2.2 卷挂载说明
| 卷挂载 | 类型 | 目的 | 变更 |
|--------|------|------|------|
| `./app:/app/app` | Bind mount | **代码目录**:容器运行时从宿主机 `/opt/wecom-it-desk/app/` 读取 Python 业务代码 | **新增** |
| `backend-uploads:/app/uploads` | Named volume | **上传文件**:持久化用户上传的文件 | 不变 |
| `${RUNTIME_LOG_HOST_DIR:-/var/log/wecom-it-desk}:/app/logs` | Bind mount | **运行日志**:持久化运行期日志,供日志管理页面读取 | 不变 |
### 2.3 路径映射关系
```
宿主机路径 → 容器路径
/opt/wecom-it-desk/app/ → /app/app/ (代码)
/opt/wecom-it-desk/backend/requirements.txt → (仅构建时使用,不挂载)
Docker volume: wecom_it_backend_uploads → /app/uploads/ (上传文件)
/var/log/wecom-it-desk/ → /app/logs/ (日志)
```
容器内 `/app/` 目录结构(运行时):
```
/app/
├── app/ ← 卷挂载(来自宿主机 ./app/)
│ ├── __init__.py
│ ├── main.py ← uvicorn app.main:app 入口
│ ├── config.py
│ ├── constants.py
│ ├── database.py
│ ├── api/
│ │ ├── auth.py ← 认证路由(曾缺失导致故障)
│ │ └── ...
│ ├── core/
│ ├── services/
│ ├── models/
│ └── ...
├── uploads/ ← Docker named volume
├── logs/ ← 宿主机目录挂载
└── (site-packages 在 /usr/local/lib/python3.12/site-packages/)
```
### 2.4 服务器目录结构调整
**变更前**(两份代码):
```
/opt/wecom-it-desk/
├── docker-compose.yml
├── app/ ← 较新代码(部署用,但不参与构建)
│ ├── main.py
│ └── ...
├── backend/
│ ├── Dockerfile
│ ├── requirements.txt
│ └── app/ ← 较旧代码(构建用,COPY . . 复制源) ← 问题根源
│ ├── main.py
│ └── ...
└── ...
```
**变更后**(单一代码源):
```
/opt/wecom-it-desk/
├── docker-compose.yml
├── app/ ← 唯一代码源(volume 挂载到容器)
│ ├── __init__.py
│ ├── main.py
│ ├── auth.py
│ ├── api/
│ ├── core/
│ ├── services/
│ └── ...
├── backend/
│ ├── Dockerfile ← 构建配置(已修改,无 COPY . .)
│ ├── requirements.txt ← 依赖声明(构建时使用)
│ └── (app/ 已删除) ← 不再需要
└── ...
```
**调整步骤**:
1. 确保 `./app/` 包含最新代码(部署包已解压,或从 git 同步)
2. 验证 `./app/` 关键文件存在:`main.py`, `auth.py`, `__init__.py`
3. 部署验证通过后 48 小时,删除 `./backend/app/`(回滚安全网保留期)
> **注意**:`./backend/app/` 在回滚期间保留。回滚时需恢复原始 Dockerfile(含 `COPY . .`),此时 `backend/app/` 作为构建上下文代码源。48 小时稳定运行后可安全删除。
---
## 3. Dockerfile 变更
### 3.1 变更后完整 Dockerfile
```dockerfile
# =============================================================================
# 企微IT智能服务台 — 后端 Docker 镜像构建文件
# =============================================================================
# 说明:基于 Python 3.12 构建后端镜像
# 变更:2025-07-10 方案C — 代码改为 volume 挂载,镜像不再 COPY 业务代码
# 用法:docker build -t wecom-it-desk-backend .
# =============================================================================
# --------------------------------------------------------------------------
# 第一阶段:构建阶段
# --------------------------------------------------------------------------
FROM python:3.12-slim AS builder
# 设置工作目录
WORKDIR /app
# 安装系统依赖(psycopg2 编译需要 + qrcode 图片处理需要 + healthcheck 需要 curl)
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc libpq-dev libjpeg-dev zlib1g-dev curl && \
rm -rf /var/lib/apt/lists/*
# 复制依赖声明文件并安装(利用 Docker 层缓存,依赖不变则不重新安装)
# 使用阿里云 PyPI 镜像(比清华镜像更快)
COPY requirements.txt .
RUN pip install --no-cache-dir \
--timeout 180 \
--retries 5 \
-i https://mirrors.aliyun.com/pypi/simple/ \
--trusted-host mirrors.aliyun.com \
-r requirements.txt
# --------------------------------------------------------------------------
# 第二阶段:运行阶段(更小的镜像体积)
# --------------------------------------------------------------------------
FROM python:3.12-slim
# 设置标签信息
LABEL maintainer="IT服务台开发团队"
LABEL description="企微IT智能服务台后端服务"
LABEL changelog="2025-07-10: 移除 COPY . .,代码改为 volume 挂载"
# 安装运行时依赖(psycopg2 运行时需要 libpq + healthcheck 需要 curl)
RUN apt-get update && \
apt-get install -y --no-install-recommends libpq5 curl && \
rm -rf /var/lib/apt/lists/*
# 设置工作目录
WORKDIR /app
# 禁止 Python 写入 __pycache__(防止污染宿主机代码目录)
ENV PYTHONDONTWRITEBYTECODE=1
# 从构建阶段复制已安装的 Python 包
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin
# 业务代码通过 docker-compose volumes 挂载(./app:/app/app),不再 COPY 进镜像
# 暴露端口
EXPOSE 8000
# 启动命令(Docker Compose 中会覆盖)
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
```
### 3.2 变更明细
| 行号 | 原始内容 | 变更后 | 操作 |
|------|----------|--------|------|
| 6 | `# 用法:...` | 增加 `# 变更:2025-07-10 方案C ...` | 修改注释 |
| 38 | `LABEL description=...` | 增加 `LABEL changelog=...` | 新增标签 |
| 46-47 | (无) | `ENV PYTHONDONTWRITEBYTECODE=1` | **新增** |
| 52-53 | `# 复制项目代码`
`COPY . .` | `# 业务代码通过 docker-compose volumes 挂载...` | **删除 COPY . .**,替换为注释 |
**保留不变的部分**:
- 第一阶段(builder):`COPY requirements.txt .` + `pip install` — 依赖安装逻辑不变
- 第二阶段:`COPY --from=builder` — 从 builder 复制 site-packages 逻辑不变
- `EXPOSE 8000` + `CMD` — 启动命令不变
---
## 4. 部署流程对比
### 4.1 代码更新流程(仅业务代码变更,无依赖变化)
| 步骤 | 变更前(镜像烘焙模式) | 变更后(卷挂载模式) |
|------|----------------------|---------------------|
| 1 | 上传/解压代码到 `./app/` | 上传/解压代码到 `./app/` |
| 2 | 手动同步到 `./backend/app/`(**易遗漏,根因**) | ~~无需同步~~ |
| 3 | `docker compose build --no-cache backend`(3-5 分钟) | ~~无需重建镜像~~ |
| 4 | `docker compose up -d backend`(重启容器) | `docker compose restart backend`(10 秒) |
| 5 | 等待健康检查通过(30-40 秒) | 等待健康检查通过(10-15 秒) |
| **总计** | **4-6 分钟** | **15-30 秒** |
**变更后代码更新命令**:
```bash
# 1. 更新代码(解压部署包或 git pull)
cd /opt/wecom-it-desk
tar -xf /tmp/deploy-backend.tar -C ./
# 2. 重启容器(无需重建镜像)
docker compose restart backend
# 3. 等待启动 + 验证
sleep 15
curl -sf http://localhost:8000/health && echo "OK" || echo "FAIL"
```
### 4.2 依赖更新流程(requirements.txt 变化)
| 步骤 | 变更前 | 变更后 |
|------|--------|--------|
| 1 | 更新 `./backend/requirements.txt` | 更新 `./backend/requirements.txt` |
| 2 | `docker compose build --no-cache backend` | `docker compose build backend`(可用缓存) |
| 3 | `docker compose up -d backend` | `docker compose up -d backend` |
| 4 | 验证 | 验证 |
| **总计** | **3-5 分钟** | **2-4 分钟**(Docker 层缓存命中时更快) |
> **注意**:依赖更新流程变化不大,因为依赖仍然烘焙在镜像中。但代码更新不再需要重建镜像,这是主要改进。
### 4.3 流程对比总结
```
变更前:代码更新 = 解压代码 → 同步到 backend/app/ → 重建镜像 → 重启容器
↑ 易出错
变更后:代码更新 = 解压代码 → 重启容器
↑ 简单可靠
```
---
## 5. 回滚策略(CRITICAL)
### 5.0 版本固定策略(部署前执行,增强回滚能力)
卷挂载后镜像不含业务代码,回滚时缺少"已知可用状态"的锚点。通过在合并成功时打双 tag,可将回滚从分钟级降到秒级。
#### 5.0.1 前提条件
服务器 `/opt/wecom-it-desk/app/` 目录需要是 git 仓库(或从远程 clone):
```bash
# 检查服务器上 app/ 是否有 .git
cd /opt/wecom-it-desk && git rev-parse --git-dir 2>/dev/null && echo "OK: git 仓库已初始化" || echo "WARN: 需要 git init 或 clone"
```
> 如果当前没有 git 仓库,部署前先执行:
> ```bash
> cd /opt/wecom-it-desk && git init && git add -A && git commit -m "baseline: 方案C 部署前快照"
> ```
#### 5.0.2 双 Tag 固定
| Tag 类型 | 何时打 | 固定了什么 | 命令 |
|---------|--------|-----------|------|
| **Git tag**(代码版本) | 每次代码合并成功后、部署前 | 代码快照 | `cd /opt/wecom-it-desk && git tag deploy-$(date +%Y%m%d-%H%M)` |
| **Docker tag**(依赖版本) | `requirements.txt` 有变更且 `docker compose build` 成功后 | 依赖镜像(site-packages) | `docker tag wecom-it-desk-backend:latest wecom-it-desk-backend:deps-$(date +%Y%m%d-%H%M)` |
**关键规则**:
- Docker tag **不需要每次合并都打** — 只在 `requirements.txt` 有变化时才需要
- 代码变了但依赖没变 → 只打 git tag 即可
- 依赖变了(新增/删除/升级 pip 包)→ 两个 tag 都打
#### 5.0.3 快速回滚路径(版本固定后可用)
有了双 tag 后,回滚分为三种路径:
| 回滚类型 | 触发场景 | 操作步骤 | 耗时 |
|---------|---------|---------|------|
| **代码回滚**(99% 的情况) | 代码有 bug,需退到上一版本 | `git checkout <上一个 deploy-tag>` → `docker compose restart backend` | **3 秒** |
| **依赖回滚**(极少) | 新依赖导致问题 | 改 compose `image:` 指向 `<上一个 deps-tag>` → `docker compose up -d backend` | **10 秒** |
| **完整回滚**(代码+依赖都退) | 需退到更早的整体状态 | 两个 tag 都切 | **10 秒** |
**快速回滚命令(代码回滚)**:
```bash
#!/bin/bash
# 快速代码回滚(需提前打好 git tag)
# 用法: bash fast-rollback.sh [git-tag-name]
# 不带参数则回滚到上一个 deploy tag
cd /opt/wecom-it-desk
if [ -z "$1" ]; then
# 自动找上一个 deploy tag
PREV_TAG=$(git tag -l "deploy-*" --sort=-creatordate | sed -n '2p')
if [ -z "$PREV_TAG" ]; then
PREV_TAG=$(git tag -l "deploy-*" --sort=-creatordate | sed -n '1p')
fi
echo "回滚到: $PREV_TAG"
git checkout "$PREV_TAG"
else
echo "回滚到: $1"
git checkout "$1"
fi
docker compose restart backend
sleep 5
# 验证
curl -sf http://localhost:8000/health && echo "PASS: 服务已恢复" || echo "FAIL: 需执行完整回滚(见 §5.2)"
```
**快速回滚命令(依赖回滚)**:
```bash
#!/bin/bash
# 快速依赖回滚(需提前打好 docker tag)
# 用法: bash fast-rollback-deps.sh
cd /opt/wecom-it-desk
if [ -z "$1" ]; then
echo "用法: bash fast-rollback-deps.sh "
echo "可用 deps tag:"
docker images --format "{{.Repository}}:{{.Tag}}" | grep "deps-"
exit 1
fi
# 修改 compose 使用指定 tag 镜像
sed -i "s|image: wecom-it-desk-backend:.*|image: wecom-it-desk-backend:$1|" docker-compose.yml
docker compose up -d backend
sleep 10
# 验证
curl -sf http://localhost:8000/health && echo "PASS: 服务已恢复" || echo "FAIL: 需执行完整回滚(见 §5.2)"
```
#### 5.0.4 回滚路径选择决策
```
需要回滚
├── 有 git/docker tag?
│ ├── 是 → 先尝试快速回滚(§5.0.3)
│ │ ├── 快速回滚成功 → 完成(秒级)
│ │ └── 快速回滚失败 → 执行完整回滚(§5.2)
│ └── 否 → 直接执行完整回滚(§5.2)
└── 问题涉及配置文件(docker-compose.yml / Dockerfile)?
└── 是 → 直接执行完整回滚(§5.2),恢复配置文件备份
```
### 5.1 回滚触发条件
满足以下 **任一** 条件即触发回滚:
| # | 触发条件 | 检测方法 |
|---|----------|----------|
| 1 | 容器启动失败(反复重启) | `docker compose ps backend` 状态为 `restarting` 或 `unhealthy` |
| 2 | 健康检查连续失败 | `curl -sf http://localhost:8000/health` 返回非 200 |
| 3 | 关键模块导入失败(如 auth) | `docker exec wecom_it_backend python -c "from app.auth import router"` 报错 |
| 4 | 卷挂载路径不存在或权限拒绝 | 容器日志出现 `ModuleNotFoundError` 或 `PermissionError` |
| 5 | 业务接口大面积 500 错误 | Nginx 日志或后端日志大量 500 状态码 |
### 5.2 回滚步骤(完整命令序列)
以下命令可通过 jumpserver-V2 在服务器 10.90.5.110 上执行:
```bash
#!/bin/bash
# =============================================================================
# 回滚脚本:卷挂载方案 → 镜像烘焙方案
# 执行方式:通过 jumpserver-V2 在 10.90.5.110 上执行
# 前提:备份文件存在(部署时已创建 .bak.{TIMESTAMP} 后缀文件)
# =============================================================================
set -e
cd /opt/wecom-it-desk
echo "===== 回滚开始: $(date) ====="
# --------------------------------------------------------------------------
# 步骤 1: 查找并恢复备份的配置文件
# --------------------------------------------------------------------------
echo ">>> [1/6] 恢复配置文件..."
# 查找最新的 docker-compose.yml 备份
COMPOSE_BAK=$(ls -t /opt/wecom-it-desk/docker-compose.yml.bak.* 2>/dev/null | head -1)
if [ -z "$COMPOSE_BAK" ]; then
echo "ERROR: 未找到 docker-compose.yml 备份文件!"
echo "可手动从 git 恢复: git checkout -- docker-compose.yml"
exit 1
fi
cp "$COMPOSE_BAK" /opt/wecom-it-desk/docker-compose.yml
echo " 已恢复 docker-compose.yml <- $COMPOSE_BAK"
# 查找最新的 Dockerfile 备份
DOCKERFILE_BAK=$(ls -t /opt/wecom-it-desk/backend/Dockerfile.bak.* 2>/dev/null | head -1)
if [ -z "$DOCKERFILE_BAK" ]; then
echo "ERROR: 未找到 Dockerfile 备份文件!"
echo "可手动从 git 恢复: git checkout -- backend/Dockerfile"
exit 1
fi
cp "$DOCKERFILE_BAK" /opt/wecom-it-desk/backend/Dockerfile
echo " 已恢复 Dockerfile <- $DOCKERFILE_BAK"
# --------------------------------------------------------------------------
# 步骤 2: 确保 backend/app/ 存在(回滚安全网)
# --------------------------------------------------------------------------
echo ">>> [2/6] 检查 backend/app/ 目录..."
if [ ! -d /opt/wecom-it-desk/backend/app/ ] || [ -z "$(ls -A /opt/wecom-it-desk/backend/app/ 2>/dev/null)" ]; then
echo " backend/app/ 不存在或为空,从 app/ 同步代码..."
mkdir -p /opt/wecom-it-desk/backend/app/
cp -r /opt/wecom-it-desk/app/* /opt/wecom-it-desk/backend/app/
cp -r /opt/wecom-it-desk/app/.* /opt/wecom-it-desk/backend/app/ 2>/dev/null || true
echo " 已同步代码到 backend/app/"
else
echo " backend/app/ 已存在,跳过同步"
fi
# --------------------------------------------------------------------------
# 步骤 3: 重新构建镜像(使用原始 Dockerfile,含 COPY . .)
# --------------------------------------------------------------------------
echo ">>> [3/6] 重新构建后端镜像..."
docker compose build --no-cache backend
# --------------------------------------------------------------------------
# 步骤 4: 重启容器
# --------------------------------------------------------------------------
echo ">>> [4/6] 重启后端容器..."
docker compose up -d backend
# --------------------------------------------------------------------------
# 步骤 5: 等待服务就绪
# --------------------------------------------------------------------------
echo ">>> [5/6] 等待服务启动..."
echo " 等待 45 秒(healthcheck start_period)..."
sleep 45
# --------------------------------------------------------------------------
# 步骤 6: 验证
# --------------------------------------------------------------------------
echo ">>> [6/6] 验证回滚结果..."
# 容器状态
echo ""
echo "--- 容器状态 ---"
docker compose ps backend
# 健康检查
echo ""
echo "--- 健康检查 ---"
if curl -sf http://localhost:8000/health > /dev/null 2>&1; then
echo " PASS: /health 返回正常"
else
echo " WARN: /health 未就绪,再等待 15 秒..."
sleep 15
if curl -sf http://localhost:8000/health > /dev/null 2>&1; then
echo " PASS: /health 返回正常(延迟就绪)"
else
echo " FAIL: /health 仍然失败"
echo " 查看日志: docker compose logs --tail=50 backend"
fi
fi
# Auth 模块验证
echo ""
echo "--- Auth 模块验证 ---"
if docker exec wecom_it_backend python -c "from app.auth import router; print('auth OK')" 2>/dev/null; then
echo " PASS: auth 模块可导入"
else
echo " FAIL: auth 模块导入失败"
echo " 查看日志: docker compose logs --tail=50 backend"
fi
# 最近日志
echo ""
echo "--- 最近 20 行日志 ---"
docker compose logs --tail=20 backend
echo ""
echo "===== 回滚完成: $(date) ====="
echo ""
echo "如回滚后仍有问题,请检查:"
echo " 1. backend/app/ 代码是否完整: ls -la /opt/wecom-it-desk/backend/app/"
echo " 2. 镜像构建是否成功: docker images | grep wecom-it-desk-backend"
echo " 3. 容器日志: docker compose logs -f backend"
```
### 5.3 回滚后验证
| 验证项 | 命令 | 预期结果 |
|--------|------|----------|
| 容器运行状态 | `docker compose ps backend` | Status 为 `Up (healthy)` |
| 健康检查 | `curl -sf http://localhost:8000/health` | 返回 `{"status":"healthy"}` 或类似 |
| Auth 模块 | `docker exec wecom_it_backend python -c "from app.auth import router; print('OK')"` | 输出 `auth OK` |
| Auth API | `curl -sf http://localhost:8000/api/auth/wecom/login -X POST -H "Content-Type: application/json" -d '{}'` | 返回 JSON(非 500) |
| Nginx 代理 | `curl -sf http://localhost:80/itdesk/health` | 返回正常 |
| 日志无异常 | `docker compose logs --tail=50 backend` | 无 `ModuleNotFoundError` / `ImportError` |
### 5.4 回滚时间预估
| 回滚路径 | 适用场景 | 耗时 | 前提 |
|---------|---------|------|------|
| **快速回滚(代码)** | 代码 bug,退到上一版本 | **3 秒** | 已打 git tag(§5.0) |
| **快速回滚(依赖)** | 新依赖导致问题 | **10 秒** | 已打 docker tag(§5.0) |
| **完整回滚** | 配置文件变更/快速回滚失败 | **3.5-5.5 分钟** | 备份文件存在 |
**完整回滚各步骤耗时**:
| 步骤 | 耗时 | 说明 |
|------|------|------|
| 恢复配置文件 | 5 秒 | cp 命令 |
| 检查/同步 backend/app/ | 5-30 秒 | 如需同步代码 |
| 重建镜像 | 2-4 分钟 | `docker compose build --no-cache` |
| 重启容器 | 10 秒 | `docker compose up -d` |
| 等待就绪 | 45 秒 | healthcheck start_period |
| 验证 | 15 秒 | curl + docker exec |
| **总计** | **3.5-5.5 分钟** | |
> **建议**:始终先尝试快速回滚(秒级),失败后再执行完整回滚(分钟级)。版本固定策略(§5.0)是快速回滚的前提。
---
## 6. 风险评估
### 6.1 风险矩阵
| # | 风险 | 影响 | 概率 | 等级 | 对策 |
|---|------|------|------|------|------|
| R1 | `__pycache__` 污染宿主机代码目录 | 权限错误 / 缓存过期导致行为异常 | 中 | 中 | Dockerfile 设置 `PYTHONDONTWRITEBYTECODE=1` |
| R2 | 宿主机代码被意外修改导致运行中服务异常 | 服务不稳定 | 低 | 中 | 限制 `/opt/wecom-it-desk/app/` 目录写权限;部署外时间窗口操作 |
| R3 | `backend/app/` 被提前删除导致回滚失败 | 回滚无法执行 | 低 | 高 | 48 小时内不删除;回滚脚本含自动同步逻辑 |
| R4 | 卷挂载路径与现有挂载冲突 | 容器启动失败 | 极低 | 低 | `/app/app` 与 `/app/uploads`、`/app/logs` 无交集 |
| R5 | requirements.txt 与代码不同步(新增依赖未安装) | `ImportError` 运行时崩溃 | 低 | 中 | 部署前对比 requirements.txt 差异;新增依赖时先重建镜像 |
| R6 | Docker Compose 版本不兼容 bind mount | 挂载行为异常 | 极低 | 低 | 部署前验证 `docker compose version` |
| R7 | 宿主机代码目录包含敏感文件(.env 等)被容器读取 | 信息泄露 | 低 | 中 | 部署包不含 `.env`;`.dockerignore` 已排除敏感文件 |
| R8 | 容器以 root 运行,挂载目录权限过松 | 安全风险 | 低 | 低 | 长期可考虑添加 `user:` 指令;短期可接受 |
### 6.2 详细风险分析
#### R1: `__pycache__` 污染
**场景**:容器运行时 Python 默认在 `__pycache__/` 目录下写入 `.pyc` 字节码缓存。由于代码目录是 volume 挂载,这些文件会写入宿主机 `/opt/wecom-it-desk/app/__pycache__/`。
**影响**:
- 如果宿主机和容器 Python 版本不同,`.pyc` 文件可能不兼容
- `__pycache__` 目录可能因权限问题导致写入失败(静默失败,不影响运行但产生日志噪音)
- 代码更新后 stale `.pyc` 可能导致旧代码被执行(Python 3.12 已大幅改善此问题,但仍有边缘情况)
**对策**:Dockerfile 设置 `ENV PYTHONDONTWRITEBYTECODE=1`,完全禁止 `.pyc` 写入。生产环境不需要字节码缓存(uvicorn 启动时编译一次即可)。
#### R2: 宿主机代码意外修改
**场景**:卷挂载是双向的(默认 read-write),宿主机上的文件修改会实时反映到容器中。
**影响**:如果在服务运行期间有人修改了 `/opt/wecom-it-desk/app/` 中的代码文件,可能导致运行中服务行为异常(虽然 uvicorn 不带 `--reload` 不会自动重载,但下次 import 可能读到修改后的代码)。
**对策**:
- 生产环境 docker-compose.yml 的 command 不含 `--reload`,代码修改不会自动生效
- 限制 `/opt/wecom-it-desk/app/` 目录的写权限
- 所有代码变更通过正式部署流程进行
#### R3: backend/app/ 提前删除
**场景**:部署完成后立即删除 `backend/app/`,若随后需要回滚,原始 Dockerfile 的 `COPY . .` 找不到代码。
**对策**:
- 回滚脚本(5.2 节)包含自动同步逻辑:若 `backend/app/` 不存在,从 `app/` 复制
- 建议 48 小时稳定运行后再删除 `backend/app/`
#### R5: 依赖与代码不同步
**场景**:开发者在新代码中引入了新的第三方库(如 `import httpx`),但未更新 `requirements.txt` 或未重建镜像。
**影响**:容器启动时 `ImportError: No module named 'httpx'`。
**对策**:
- 部署前检查:`diff` 对比新旧 `requirements.txt`
- 若有变化,先执行 `docker compose build backend` 再重启
- 在部署流程中增加自动化检查
---
## 7. 任务分解
### 7.1 部署步骤(有序)
| 步骤 | 名称 | 命令 / 操作 | 依赖 | 耗时 | 验证方法 |
|------|------|-------------|------|------|----------|
| S1 | 前置验证与备份 | 见下方命令块 S1 | 无 | 2 分钟 | 备份文件存在;当前 `/health` 返回正常 |
| S2 | 代码同步与验证 | 见下方命令块 S2 | S1 | 1 分钟 | `./app/main.py`、`./app/api/auth.py` 存在 |
| S3 | 修改 Dockerfile | 见下方命令块 S3 | S2 | 1 分钟 | `grep -c "COPY . ." Dockerfile` 返回 0 |
| S4 | 修改 docker-compose.yml | 见下方命令块 S4 | S3 | 1 分钟 | `grep "app:/app/app" docker-compose.yml` 返回匹配 |
| S5 | 重建镜像并重启 | 见下方命令块 S5 | S4 | 3 分钟 | `docker compose ps backend` 状态为 `Up` |
| S6 | 部署后验证 | 见下方命令块 S6 | S5 | 2 分钟 | 健康检查通过 + auth 模块可导入 |
| S7 | 清理(48h 后) | 见下方命令块 S7 | S6 + 48h | 1 分钟 | `backend/app/` 已删除 |
### 7.2 完整命令块
**S1: 前置验证与备份**
```bash
#!/bin/bash
set -e
cd /opt/wecom-it-desk
# 1. 验证当前服务正常
echo ">>> 验证当前服务状态..."
curl -sf http://localhost:8000/health > /dev/null && echo " PASS: 当前服务正常" || { echo " FAIL: 当前服务异常,请先修复再部署"; exit 1; }
# 2. 备份配置文件
BACKUP_TS=$(date +%Y%m%d%H%M%S)
cp /opt/wecom-it-desk/docker-compose.yml /opt/wecom-it-desk/docker-compose.yml.bak.${BACKUP_TS}
cp /opt/wecom-it-desk/backend/Dockerfile /opt/wecom-it-desk/backend/Dockerfile.bak.${BACKUP_TS}
echo " PASS: 备份完成 (timestamp: ${BACKUP_TS})"
# 3. 记录回滚信息
cat > /opt/wecom-it-desk/.rollback-info << EOF
ROLLBACK_TIMESTAMP=${BACKUP_TS}
COMPOSE_BAK=/opt/wecom-it-desk/docker-compose.yml.bak.${BACKUP_TS}
DOCKERFILE_BAK=/opt/wecom-it-desk/backend/Dockerfile.bak.${BACKUP_TS}
DEPLOY_DATE=$(date)
EOF
echo " PASS: 回滚信息已记录到 .rollback-info"
echo ""
echo "===== S1 完成 ====="
```
**S2: 代码同步与验证**
```bash
#!/bin/bash
set -e
cd /opt/wecom-it-desk
# 1. 验证 app/ 目录存在且包含关键文件
echo ">>> 验证代码目录..."
for f in app/__init__.py app/main.py app/config.py app/database.py; do
if [ -f "$f" ]; then
echo " PASS: $f 存在"
else
echo " FAIL: $f 不存在!请先解压部署包: tar -xf /tmp/deploy-backend.tar -C ./"
exit 1
fi
done
# 2. 验证 auth 模块(关键!曾因缺失导致故障)
if [ -f "app/api/auth.py" ]; then
echo " PASS: app/api/auth.py 存在"
else
echo " FAIL: app/api/auth.py 不存在!认证功能将不可用"
exit 1
fi
# 3. 统计代码文件数
FILE_COUNT=$(find app/ -name "*.py" | wc -l)
echo " INFO: app/ 目录共 ${FILE_COUNT} 个 Python 文件"
echo ""
echo "===== S2 完成 ====="
```
**S3: 修改 Dockerfile**
```bash
#!/bin/bash
set -e
DOCKERFILE=/opt/wecom-it-desk/backend/Dockerfile
echo ">>> 修改 Dockerfile..."
# 方法:直接写入完整文件(最可靠)
cat > "$DOCKERFILE" << 'DOCKERFILE_EOF'
# =============================================================================
# 企微IT智能服务台 — 后端 Docker 镜像构建文件
# =============================================================================
# 说明:基于 Python 3.12 构建后端镜像
# 变更:2025-07-10 方案C — 代码改为 volume 挂载,镜像不再 COPY 业务代码
# 用法:docker build -t wecom-it-desk-backend .
# =============================================================================
# --------------------------------------------------------------------------
# 第一阶段:构建阶段
# --------------------------------------------------------------------------
FROM python:3.12-slim AS builder
# 设置工作目录
WORKDIR /app
# 安装系统依赖(psycopg2 编译需要 + qrcode 图片处理需要 + healthcheck 需要 curl)
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc libpq-dev libjpeg-dev zlib1g-dev curl && \
rm -rf /var/lib/apt/lists/*
# 复制依赖声明文件并安装(利用 Docker 层缓存,依赖不变则不重新安装)
# 使用阿里云 PyPI 镜像(比清华镜像更快)
COPY requirements.txt .
RUN pip install --no-cache-dir \
--timeout 180 \
--retries 5 \
-i https://mirrors.aliyun.com/pypi/simple/ \
--trusted-host mirrors.aliyun.com \
-r requirements.txt
# --------------------------------------------------------------------------
# 第二阶段:运行阶段(更小的镜像体积)
# --------------------------------------------------------------------------
FROM python:3.12-slim
# 设置标签信息
LABEL maintainer="IT服务台开发团队"
LABEL description="企微IT智能服务台后端服务"
LABEL changelog="2025-07-10: 移除 COPY . .,代码改为 volume 挂载"
# 安装运行时依赖(psycopg2 运行时需要 libpq + healthcheck 需要 curl)
RUN apt-get update && \
apt-get install -y --no-install-recommends libpq5 curl && \
rm -rf /var/lib/apt/lists/*
# 设置工作目录
WORKDIR /app
# 禁止 Python 写入 __pycache__(防止污染宿主机代码目录)
ENV PYTHONDONTWRITEBYTECODE=1
# 从构建阶段复制已安装的 Python 包
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin
# 业务代码通过 docker-compose volumes 挂载(./app:/app/app),不再 COPY 进镜像
# 暴露端口
EXPOSE 8000
# 启动命令(Docker Compose 中会覆盖)
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
DOCKERFILE_EOF
# 验证
if grep -q "COPY . ." "$DOCKERFILE"; then
echo " FAIL: Dockerfile 仍包含 COPY . ."
exit 1
fi
if grep -q "PYTHONDONTWRITEBYTECODE" "$DOCKERFILE"; then
echo " PASS: PYTHONDONTWRITEBYTECODE 已设置"
else
echo " FAIL: PYTHONDONTWRITEBYTECODE 未找到"
exit 1
fi
echo " PASS: Dockerfile 已更新"
echo ""
echo "===== S3 完成 ====="
```
**S4: 修改 docker-compose.yml**
```bash
#!/bin/bash
set -e
cd /opt/wecom-it-desk
COMPOSE_FILE=/opt/wecom-it-desk/docker-compose.yml
echo ">>> 修改 docker-compose.yml..."
# 在 backend-uploads 行之前插入代码卷挂载行
# 使用 sed 的 insert (i) 命令
sed -i '/backend-uploads:\/app\/uploads/i\ - ./app:/app/app # 代码卷挂载(方案C)' "$COMPOSE_FILE"
# 验证
if grep -q "./app:/app/app" "$COMPOSE_FILE"; then
echo " PASS: 代码卷挂载已添加"
else
echo " FAIL: 代码卷挂载未找到,请手动编辑 docker-compose.yml"
echo " 在 backend 服务的 volumes: 下添加:"
echo " - ./app:/app/app"
exit 1
fi
# 显示变更后的 volumes 段
echo ""
echo " 变更后的 volumes 配置:"
sed -n '/^ backend:/,/^ [a-z]/p' "$COMPOSE_FILE" | grep -A 5 "volumes:"
echo ""
echo "===== S4 完成 ====="
```
**S5: 重建镜像并重启**
```bash
#!/bin/bash
set -e
cd /opt/wecom-it-desk
echo ">>> 重建后端镜像..."
docker compose build backend
echo " PASS: 镜像构建完成"
echo ""
echo ">>> 重启后端容器..."
docker compose up -d backend
echo " PASS: 容器已启动"
echo ""
echo ">>> 等待服务就绪 (45秒)..."
sleep 45
echo ""
echo "===== S5 完成 ====="
```
**S6: 部署后验证**
```bash
#!/bin/bash
cd /opt/wecom-it-desk
echo "===== 部署后验证 ====="
echo ""
# 1. 容器状态
echo "--- 1. 容器状态 ---"
docker compose ps backend
echo ""
# 2. 健康检查
echo "--- 2. 健康检查 ---"
if curl -sf http://localhost:8000/health; then
echo ""
echo " PASS: /health 正常"
else
echo " FAIL: /health 异常"
fi
echo ""
# 3. Auth 模块验证(曾因缺失导致故障)
echo "--- 3. Auth 模块验证 ---"
if docker exec wecom_it_backend python -c "from app.auth import router; print(' auth module: OK')" 2>/dev/null; then
echo " PASS: auth 模块可导入"
else
echo " FAIL: auth 模块导入失败"
echo " 查看日志: docker compose logs --tail=50 backend"
fi
echo ""
# 4. 卷挂载验证
echo "--- 4. 卷挂载验证 ---"
if docker exec wecom_it_backend ls -la /app/app/main.py > /dev/null 2>&1; then
echo " PASS: /app/app/main.py 可访问(卷挂载正常)"
else
echo " FAIL: /app/app/main.py 不可访问(卷挂载异常)"
fi
echo ""
# 5. 代码来源验证(确认代码来自宿主机而非镜像)
echo "--- 5. 代码来源验证 ---"
HOST_HASH=$(md5sum /opt/wecom-it-desk/app/main.py | awk '{print $1}')
CONTAINER_HASH=$(docker exec wecom_it_backend md5sum /app/app/main.py 2>/dev/null | awk '{print $1}')
if [ "$HOST_HASH" = "$CONTAINER_HASH" ]; then
echo " PASS: 宿主机与容器代码一致 (md5: ${HOST_HASH})"
else
echo " WARN: 宿主机与容器代码不一致"
echo " 宿主机: $HOST_HASH"
echo " 容器: $CONTAINER_HASH"
fi
echo ""
# 6. 日志检查
echo "--- 6. 最近 20 行日志 ---"
docker compose logs --tail=20 backend
echo ""
echo "===== 验证完成 ====="
echo ""
echo "如全部 PASS,部署成功。"
echo "如出现 FAIL,请执行回滚脚本(见回滚策略章节)。"
```
**S7: 清理(48 小时后执行)**
```bash
#!/bin/bash
set -e
cd /opt/wecom-it-desk
echo ">>> 清理旧代码目录..."
echo " 注意:仅在部署成功 48 小时后执行此步骤!"
echo ""
# 确认服务稳定
curl -sf http://localhost:8000/health > /dev/null && echo " 服务正常" || { echo " 服务异常,取消清理"; exit 1; }
# 删除 backend/app/(不再需要)
if [ -d /opt/wecom-it-desk/backend/app/ ]; then
echo " 删除 backend/app/..."
rm -rf /opt/wecom-it-desk/backend/app/
echo " PASS: backend/app/ 已删除"
else
echo " INFO: backend/app/ 已不存在,跳过"
fi
# 可选:删除备份文件(建议保留 7 天)
# find /opt/wecom-it-desk/ -name "*.bak.*" -mtime +7 -delete
echo ""
echo "===== 清理完成 ====="
```
### 7.3 任务依赖图
```mermaid
graph TD
S1[S1: 前置验证与备份
2分钟] --> S2[S2: 代码同步与验证
1分钟]
S2 --> S3[S3: 修改 Dockerfile
1分钟]
S3 --> S4[S4: 修改 docker-compose.yml
1分钟]
S4 --> S5[S5: 重建镜像并重启
3分钟]
S5 --> S6[S6: 部署后验证
2分钟]
S6 --> S7[S7: 清理旧代码
48小时后 / 1分钟]
S6 -.->|如验证失败| ROLLBACK[执行回滚脚本
3.5-5.5分钟]
ROLLBACK -.->|回滚后| RECOVERY[恢复到变更前状态]
style S1 fill:#4CAF50,color:#fff
style S5 fill:#FF9800,color:#fff
style S6 fill:#2196F3,color:#fff
style S7 fill:#9E9E9E,color:#fff
style ROLLBACK fill:#f44336,color:#fff
```
### 7.4 预估总时间
| 阶段 | 步骤 | 耗时 |
|------|------|------|
| 准备 | S1 + S2 | 3 分钟 |
| 变更 | S3 + S4 | 2 分钟 |
| 部署 | S5 | 3 分钟 |
| 验证 | S6 | 2 分钟 |
| **总计** | S1-S6 | **约 10 分钟** |
| 清理 | S7(48h 后) | 1 分钟 |
| **回滚(如需)** | 回滚脚本 | **3.5-5.5 分钟** |
---
## 8. 附录
### 8.1 架构对比图
```mermaid
graph LR
subgraph BEFORE["变更前:镜像烘焙模式"]
direction TB
B1["宿主机 app/
(新代码)"] -.->|"不同步!"| B2["宿主机 backend/app/
(旧代码)"]
B2 -->|"COPY . ."| B3["Docker 镜像
(含代码)"]
B3 --> B4["容器 /app/app/"]
end
subgraph AFTER["变更后:卷挂载模式"]
direction TB
A1["宿主机 app/
(唯一代码源)"] -->|"volume mount"| A2["容器 /app/app/"]
A3["Docker 镜像
(仅含依赖)"] --> A2
end
BEFORE -.->|"重构"| AFTER
style BEFORE fill:#ffebee
style AFTER fill:#e8f5e9
```
### 8.2 部署时序图
```mermaid
sequenceDiagram
participant Ops as 运维人员
participant Host as 宿主机 10.90.5.110
participant Docker as Docker Engine
participant Container as Backend 容器
Note over Ops,Container: S1: 前置验证与备份
Ops->>Host: curl /health 验证当前状态
Ops->>Host: 备份 docker-compose.yml + Dockerfile
Note over Ops,Container: S2: 代码验证
Ops->>Host: 检查 app/main.py, app/api/auth.py 存在
Note over Ops,Container: S3-S4: 修改配置文件
Ops->>Host: 重写 Dockerfile(删除 COPY . .)
Ops->>Host: 编辑 docker-compose.yml(添加 ./app:/app/app)
Note over Ops,Container: S5: 重建并重启
Ops->>Docker: docker compose build backend
Docker->>Docker: 构建镜像(仅 site-packages,无业务代码)
Ops->>Docker: docker compose up -d backend
Docker->>Container: 创建容器
Docker->>Host: 挂载 ./app → /app/app
Container->>Host: 运行时读取 /opt/wecom-it-desk/app/ 代码
Container->>Container: uvicorn app.main:app 启动
Note over Ops,Container: S6: 验证
Ops->>Container: curl /health
Ops->>Container: docker exec ... from app.auth import router
Ops->>Container: md5sum 对比宿主机与容器代码
```
### 8.3 关键设计决策
| 决策点 | 选择 | 理由 |
|--------|------|------|
| 挂载路径 `./app` vs `./backend/app` | `./app` | 部署包 tar 解压到 `./app/`,与现有部署流程兼容;无需修改打包脚本 |
| `PYTHONDONTWRITEBYTECODE` 位置 | Dockerfile `ENV` | 属于镜像属性,所有基于此镜像的容器都生效 |
| `backend/app/` 删除时机 | 48 小时后 | 保留回滚安全网;回滚脚本含自动同步逻辑作为兜底 |
| 卷挂载读写模式 | 默认 read-write | 虽然生产不需要写入代码目录,但 read-only 模式可能导致边缘问题;用 `PYTHONDONTWRITEBYTECODE` 替代 |
| 是否挂载 alembic/scripts | 否 | 生产 command 跳过迁移;需要时可手动 `docker exec` 执行 |
### 8.4 Shared Knowledge(工程师注意事项)
- **代码唯一源**:`/opt/wecom-it-desk/app/` 是生产环境唯一的代码目录,所有代码更新只操作此目录
- **镜像不含代码**:重建镜像只更新依赖包,代码通过 volume 挂载提供
- **部署区分**:代码更新 → `docker compose restart`;依赖更新 → `docker compose build && docker compose up -d`
- **回滚文件**:部署时创建的 `.bak.{TIMESTAMP}` 文件是回滚的关键,不要手动删除
- **回滚信息**:`/opt/wecom-it-desk/.rollback-info` 文件记录了备份文件路径,回滚时可直接读取
- **dev 环境**:本地开发使用 `docker-compose.dev.yml`,已使用 `./backend/app:/app/app` 挂载,不受此次变更影响
- **健康检查**:容器 healthcheck 的 `start_period` 为 40 秒,回滚验证需等待至少 45 秒
---
> **文档结束**
> 如有疑问请联系架构师 高见远。
> 回滚策略已完整覆盖所有场景,可直接通过 jumpserver-V2 执行。