本提交为 .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-*/
44 KiB
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 部分):
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/ 已删除) ← 不再需要
└── ...
调整步骤:
- 确保
./app/包含最新代码(部署包已解压,或从 git 同步) - 验证
./app/关键文件存在:main.py,auth.py,__init__.py - 部署验证通过后 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 backend(3-5 分钟) |
|
| 4 | docker compose up -d backend(重启容器) |
docker compose restart backend(10 秒) |
| 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 状态为 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 上执行:
#!/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 分钟 |
| 清理 | S7(48h 后) | 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 执行。