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