# 任务说明书 — 后端部署卷挂载改造 > **版本**: v1.0 | **日期**: 2026-07-10 --- ## 📋 基本信息 | 项目 | 内容 | |------|------| | **任务名称** | 后端部署架构改造:镜像烘焙 → 代码卷挂载 | | **任务ID** | #107 | | **优先级** | 🟠 P1 | | **类型** | 部署优化 | | **状态** | 待开始 | | **负责人** | 宋献 | | **创建日期** | 2026-07-10 | | **计划完成日期** | 2026-07-11 | --- ## 📥 输入项来源 ### 产品需求 | 来源文档 | 相关章节 | 说明 | |----------|----------|------| | 无 | — | 运维需求,非产品功能 | ### 技术架构 | 来源文档 | 相关章节 | 说明 | |----------|----------|------| | `02-技术文档/技术架构/IT智能服务台-系统架构设计文档v2.md` | §5.0 部署模式演进 | 方案 C 设计说明 | | `04-运维文档/部署运维/卷挂载重构方案.md` | 全文 | 完整 8 章节方案(架构师高见远产出) | | `04-运维文档/部署运维/00-标准故障排查手册.md` | §1.4 + CASE-20260710-02 | 部署前同步检查清单 + 镜像缺文件案例 | ### 事故背景 | 日期 | 事故 | 根因 | |------|------|------| | 2026-07-07 | 认证路由缺失 | Docker 镜像未重新构建 | | 2026-07-10 | auth.py 缺失导致认证全断 | 镜像从 backend/app/(旧代码)构建,两份代码不同步 | **共同根因**:代码通过 `COPY . .` 烘焙进镜像,服务器两份代码不同步导致构建出缺文件的镜像。 --- ## 📤 输出成果要求 ### 交付物清单 | # | 交付物 | 类型 | 说明 | |---|--------|------|------| | 1 | `backend/Dockerfile` | 配置 | 删除 `COPY . .`,新增 `ENV PYTHONDONTWRITEBYTECODE=1` | | 2 | `docker-compose.yml` | 配置 | backend 服务 volumes 新增 `./app:/app/app` | | 3 | 服务器代码目录调整 | 运维 | `app/` 成为唯一代码源,`backend/app/` 保留 48h 后删除 | | 4 | 部署验证通过 | 验证 | 健康检查 + auth 模块 + 卷挂载 + 代码一致性 | ### 代码要求 - Dockerfile 变更:仅删除 `COPY . .`(第 53 行),新增 `ENV PYTHONDONTWRITEBYTECODE=1`,其余不变 - docker-compose.yml 变更:backend 服务 volumes 段新增一行 `./app:/app/app`,插入到 `backend-uploads` 行之前 - 不涉及任何 Python 业务代码变更 ### 文档要求 - 架构设计文档已更新(§5.0 部署模式演进,v2.1) - 项目管理主文档已更新(看板新增 #107) - 完整方案文档已归档:`docs/04-运维文档/部署运维/卷挂载重构方案.md` --- ## 🔧 验证方式 ### 功能验证 | 验证项 | 验证方法 | 预期结果 | |--------|----------|-----------| | 容器运行状态 | `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')"` | 输出 `OK`(曾因缺失导致故障) | | 卷挂载 | `docker exec wecom_it_backend ls -la /app/app/main.py` | 文件存在且可读 | | 代码一致性 | `md5sum` 对比宿主机与容器内 `main.py` | md5 值一致 | | 日志检查 | `docker compose logs --tail=50 backend` | 无 `ModuleNotFoundError` / `ImportError` | ### 安全验证 | 验证项 | 验证方法 | 预期结果 | |--------|----------|-----------| | `COPY . .` 已移除 | `grep -c "COPY . ." backend/Dockerfile` | 返回 0 | | 卷挂载已添加 | `grep "app:/app/app" docker-compose.yml` | 返回匹配 | | `__pycache__` 禁止 | `docker exec wecom_it_backend python -c "import sys; print(sys.dont_write_bytecode)"` | 输出 `True` | ### 性能验证 | 验证项 | 验证方法 | 预期结果 | |--------|----------|-----------| | 容器重启速度 | 部署后 `docker compose restart backend` + `time` 计时 | < 15 秒(此前需 4-6 分钟) | | 健康检查就绪 | 重启后 `curl /health` 轮询 | 15 秒内就绪 | --- ## 🔄 回滚策略(CRITICAL) ### 回滚触发条件 满足以下 **任一** 条件即触发回滚: | # | 触发条件 | 检测方法 | |---|----------|----------| | 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 状态码 | ### 回滚步骤(可通过 jumpserver-V2 执行) ```bash #!/bin/bash # ============================================================================= # 回滚脚本:卷挂载方案 → 镜像烘焙方案 # 执行方式:通过 jumpserver-V2 在 10.90.5.110 上执行 # 前提:备份文件存在(部署时已创建 .bak.{TIMESTAMP} 后缀文件) # ============================================================================= set -e cd /opt/wecom-it-desk echo "===== 回滚开始: $(date) =====" # --- 步骤 1: 恢复备份的配置文件 --- echo ">>> [1/6] 恢复配置文件..." 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_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 "--- 容器状态 ---" docker compose ps backend 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 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 "===== 回滚完成: $(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" ``` ### 回滚后验证 | 验证项 | 命令 | 预期结果 | |--------|------|----------| | 容器运行状态 | `docker compose ps backend` | Status 为 `Up (healthy)` | | 健康检查 | `curl -sf http://localhost:8000/health` | 返回正常 | | Auth 模块 | `docker exec wecom_it_backend python -c "from app.auth import router; print('OK')"` | 输出 `OK` | | Nginx 代理 | `curl -sf http://localhost:80/itdesk/health` | 返回正常 | | 日志无异常 | `docker compose logs --tail=50 backend` | 无 `ModuleNotFoundError` / `ImportError` | ### 回滚时间预估 | 步骤 | 耗时 | |------|------| | 恢复配置文件 | 5 秒 | | 检查/同步 backend/app/ | 5-30 秒 | | 重建镜像 | 2-4 分钟 | | 重启容器 | 10 秒 | | 等待就绪 | 45 秒 | | 验证 | 15 秒 | | **总计** | **3.5-5.5 分钟** | --- ## ✅ 完成标准 ### 验收条件 - [ ] Dockerfile 已删除 `COPY . .`,已新增 `PYTHONDONTWRITEBYTECODE=1` - [ ] docker-compose.yml 已新增 `./app:/app/app` 卷挂载 - [ ] 镜像已重建并重启成功 - [ ] 健康检查通过(`/health` 返回正常) - [ ] Auth 模块可导入(`from app.auth import router` 成功) - [ ] 卷挂载验证通过(容器内 `/app/app/main.py` 可访问) - [ ] 代码一致性验证通过(宿主机与容器 md5 一致) - [ ] 日志无 `ModuleNotFoundError` / `ImportError` - [ ] 部署 48 小时后 `backend/app/` 已清理 - [ ] 文档已更新(架构设计文档 v2.1、项目管理主文档 v2.1) ### 产出确认 - [ ] 服务器 Dockerfile 已更新 - [ ] 服务器 docker-compose.yml 已更新 - [ ] 镜像重建成功 - [ ] 容器运行正常 - [ ] 回滚脚本已验证可用 - [ ] 备份文件已创建(`.bak.{TIMESTAMP}`) - [ ] `.rollback-info` 文件已记录 --- ## 📊 工作分解 ### 子任务 | 子任务 | 负责人 | 预估工时 | 状态 | 依赖 | |--------|--------|----------|------|------| | S1: 前置验证与备份 | 宋献 | 2 分钟 | 待开始 | 无 | | S2: 代码同步与验证 | 宋献 | 1 分钟 | 待开始 | S1 | | S3: 修改 Dockerfile | 宋献 | 1 分钟 | 待开始 | S2 | | S4: 修改 docker-compose.yml | 宋献 | 1 分钟 | 待开始 | S3 | | S5: 重建镜像并重启 | 宋献 | 3 分钟 | 待开始 | S4 | | S6: 部署后验证 | 宋献 | 2 分钟 | 待开始 | S5 | | S7: 清理 backend/app/(48h 后) | 宋献 | 1 分钟 | 待开始 | S6 + 48h | ### 预估总时间 | 阶段 | 步骤 | 耗时 | |------|------|------| | 准备 | S1 + S2 | 3 分钟 | | 变更 | S3 + S4 | 2 分钟 | | 部署 | S5 | 3 分钟 | | 验证 | S6 | 2 分钟 | | **总计** | S1-S6 | **约 10 分钟** | | 清理 | S7(48h 后) | 1 分钟 | | **回滚(如需)** | 回滚脚本 | **3.5-5.5 分钟** | --- ## 📞 依赖与阻塞 ### 前置依赖 | 依赖任务 | 依赖说明 | 状态 | |----------|----------|------| | 无 | 独立运维任务 | — | ### 阻塞因素 | 阻塞项 | 影响范围 | 解决方案 | |--------|----------|-----------| | 无 | — | — | --- ## ⚠️ 风险评估 | # | 风险 | 概率 | 等级 | 对策 | |---|------|------|------|------| | R1 | `__pycache__` 污染宿主机代码目录 | 中 | 中 | Dockerfile 设置 `PYTHONDONTWRITEBYTECODE=1` | | R2 | 宿主机代码被意外修改导致运行中服务异常 | 低 | 中 | 生产不启用 `--reload`;限制目录写权限 | | R3 | `backend/app/` 被提前删除导致回滚失败 | 低 | 高 | 48 小时内不删除;回滚脚本含自动同步逻辑 | | R4 | requirements.txt 与代码不同步 | 低 | 中 | 部署前 diff 对比;新增依赖时先重建镜像 | | R5 | 卷挂载路径与现有挂载冲突 | 极低 | 低 | `/app/app` 与 `/app/uploads`、`/app/logs` 无交集 | --- ## 📈 变更记录 | 日期 | 变更内容 | 变更人 | 说明 | |------|----------|--------|------| | 2026-07-10 | 创建任务 | 宋献 | 初始版本,基于架构师高见远的卷挂载重构方案 | --- ## 📎 附件 - **完整方案文档**:`docs/04-运维文档/部署运维/卷挂载重构方案.md`(含完整命令块、时序图、风险评估) - **架构设计文档**:`docs/02-技术文档/技术架构/IT智能服务台-系统架构设计文档v2.md` §5.0 - **故障排查手册**:`docs/04-运维文档/部署运维/00-标准故障排查手册.md` §1.4 + CASE-20260710-02 - **deploy-troubleshoot skill**:`~/.workbuddy/skills/deploy-troubleshoot/SKILL.md` Step -1 部署前同步检查 - **task-intake skill**:`.workbuddy/skills/task-intake/SKILL.md` Step 3.1 部署运维前置检查 --- ## 📝 完整部署命令块 以下命令可通过 jumpserver-V2 在服务器 10.90.5.110 上按步骤执行。 ### 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 层缓存,依赖不变则不重新安装) 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 -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 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 echo "" echo "===== 清理完成 =====" ```