Files
wecom_it_smart_desk/docs/04-运维文档/部署运维/卷挂载重构方案.md
T
Simon facc04aa65 chore: docs 结构整改 + compose 双目录对齐(合并重建提交)
本提交为 .git 对象库损坏后的重建提交,内容等价于原先三个本地提交
(5e2fd4c2 / 57a53c98 / 5d7e1873)的累积结果,未做任何额外改动。

一、docs 结构整改(整改 #14)
根因:重构时新结构为 untracked 文件,执行 git stash(未带 -u)未纳入,
随后 git reset 拉回 HEAD 旧 tracked 树,导致旧树复活、新旧两棵目录
树并存于 docs/,共 791 文件、双分类体系冲突。

修复动作:
- b2 同名异主题文件改名迁移保全 9 个
- C 类 39 个孤立文件按主题正确归类
- A/B1 类 222 个重复文件删除(新结构已有内容副本)
- 9 个旧独有空目录删除
- 270 处内部引用按 verified 映射改写
- 整改记录 #14 登记于 04-运维文档/部署运维

结果:docs 791 → 569 文件,顶层仅规范 8 类 + 治理文件,单树恢复。
残留:约 20 处指向从未存在文件的陈旧死链,归入独立文档卫生任务。

二、compose 双目录对齐(消除踩坑 A)
- docker-compose.yml:nginx 前端挂载全部由根目录 frontend-*/dist
  改为 src/frontend-*/dist(h5 / agent / admin / terminal)
- docker-compose.dev.yml:dev 服务 build context 与卷同步改 src/
- 效果:本地 docker compose up 不再把根目录 stale dist 挂回,
  与线上一致,分叉隐患消除(已 docker compose config 校验通过)

防复发铁律:
- 重构须提交;仓库修复须 git stash -u 或先 commit
- 新结构须 git add 并提交,避免再次 untracked 复活
- H5 改动只动 src/frontend-h5/,禁改根目录遗留 frontend-*/
2026-08-07 22:31:32 +08:00

44 KiB
Raw Blame History

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 为 ./backendCOPY . . 复制的是 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 部分):

    volumes:
      - backend-uploads:/app/uploads
      - ${RUNTIME_LOG_HOST_DIR:-/var/log/wecom-it-desk}:/app/logs

变更后

    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

# =============================================================================
# 企微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 backend3-5 分钟) 无需重建镜像
4 docker compose up -d backend(重启容器) docker compose restart backend10 秒)
5 等待健康检查通过(30-40 秒) 等待健康检查通过(10-15 秒)
总计 4-6 分钟 15-30 秒

变更后代码更新命令

# 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):

# 检查服务器上 app/ 是否有 .git
cd /opt/wecom-it-desk && git rev-parse --git-dir 2>/dev/null && echo "OK: git 仓库已初始化" || echo "WARN: 需要 git init 或 clone"

如果当前没有 git 仓库,部署前先执行:

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 秒

快速回滚命令(代码回滚)

#!/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"

快速回滚命令(依赖回滚)

#!/bin/bash
# 快速依赖回滚(需提前打好 docker tag)
# 用法: bash fast-rollback-deps.sh <deps-tag-name>

cd /opt/wecom-it-desk

if [ -z "$1" ]; then
    echo "用法: bash fast-rollback-deps.sh <deps-tag-name>"
    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 状态为 restartingunhealthy
2 健康检查连续失败 curl -sf http://localhost:8000/health 返回非 200
3 关键模块导入失败(如 auth docker exec wecom_it_backend python -c "from app.auth import router" 报错
4 卷挂载路径不存在或权限拒绝 容器日志出现 ModuleNotFoundErrorPermissionError
5 业务接口大面积 500 错误 Nginx 日志或后端日志大量 500 状态码

5.2 回滚步骤(完整命令序列)

以下命令可通过 jumpserver-V2 在服务器 10.90.5.110 上执行:

#!/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: 前置验证与备份

#!/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: 代码同步与验证

#!/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

#!/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

#!/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: 重建镜像并重启

#!/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: 部署后验证

#!/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 小时后执行)

#!/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 任务依赖图

graph TD
    S1[S1: 前置验证与备份<br/>2分钟] --> S2[S2: 代码同步与验证<br/>1分钟]
    S2 --> S3[S3: 修改 Dockerfile<br/>1分钟]
    S3 --> S4[S4: 修改 docker-compose.yml<br/>1分钟]
    S4 --> S5[S5: 重建镜像并重启<br/>3分钟]
    S5 --> S6[S6: 部署后验证<br/>2分钟]
    S6 --> S7[S7: 清理旧代码<br/>48小时后 / 1分钟]
    
    S6 -.->|如验证失败| ROLLBACK[执行回滚脚本<br/>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 分钟
清理 S748h 后) 1 分钟
回滚(如需) 回滚脚本 3.5-5.5 分钟

8. 附录

8.1 架构对比图

graph LR
    subgraph BEFORE["变更前:镜像烘焙模式"]
        direction TB
        B1["宿主机 app/<br/>(新代码)"] -.->|"不同步!"| B2["宿主机 backend/app/<br/>(旧代码)"]
        B2 -->|"COPY . ."| B3["Docker 镜像<br/>(含代码)"]
        B3 --> B4["容器 /app/app/"]
    end
    
    subgraph AFTER["变更后:卷挂载模式"]
        direction TB
        A1["宿主机 app/<br/>(唯一代码源)"] -->|"volume mount"| A2["容器 /app/app/"]
        A3["Docker 镜像<br/>(仅含依赖)"] --> A2
    end
    
    BEFORE -.->|"重构"| AFTER
    
    style BEFORE fill:#ffebee
    style AFTER fill:#e8f5e9

8.2 部署时序图

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 执行。