Files
backend/docs/11-20260717部署事故分析.md
T
34047007@qq.com a6cd99a4ca
CI / backend (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
feat: initial commit - oncology literature search platform
OncoLit: a multi-tenant oncology literature search, feed, and
collaboration platform. Built with FastAPI + Vue 3 + PostgreSQL.
Includes PubMed pipeline, drug approvals, AI summaries, and
systematic review tools.
2026-07-27 07:59:18 +08:00

9.3 KiB
Raw Blame History

2026-07-17 部署事故分析

概述

一次安全加固部署(CSP 统一、Refresh token 告警、注册验证码、JWT 校验、注册限速、Nginx 日志持久化),因多个低级错误反复修复,耗时半天。

所有问题根因是同一个模式:以为"这样就可以了",但对 Docker 的某个细节理解有误,多步累积后全面崩溃。


问题一:tar 路径嵌套

现象

tar xzf backend_app.tar.gz -C /root/scilit/backend/ 后,文件到了 /root/scilit/backend/backend/app/...,导致 docker cp 到容器后找不到新增的代码。

根因

打包命令 tar czf backend_app.tar.gz backend/app/ — tar 包内路径前缀是 backend/。解压到 /root/scilit/backend/ 后变成 backend/backend/app/,多嵌套一层。

教训

  • tar 打包时路径前缀和 -C <dir> 的对应关系容易搞错
  • 大半夜连续多次犯同样错误,说明没有停下来确认
  • 事后验尸:容器内 grep 不到新代码时就该发现,但侥幸重新 cp 了事

正确做法

两个选一个:

# 方式 A(推荐):cd 到目录内再打包
cd backend && tar czf /tmp/backend-update.tar.gz app/

# 方式 Bstrip-components 解压
tar xzf /tmp/backend-update.tar.gz --strip-components=1 -C /root/scilit/backend/

注意事项

  • 解压后 ls /root/scilit/backend/app/ 确认路径正确
  • 不要在凌晨精神疲劳时做多步手动操作
  • 不确定路径时,先用 tar tf archive.tar.gz | head 预览包内结构

问题二:Frontend 容器重建导致 docker cp 的文件丢失

现象

  • 第一次 docker cp 前端 → 一切正常
  • 改 compose 文件加 nginx 日志 volume → 执行 docker compose up -d --no-deps frontend
  • 容器重建 → 从旧镜像启动 → 之前 cp 的文件全部消失
  • 前端回到旧版本,首页热门标签加载逻辑缺失 → 显示空白

根因

docker cp 修改的是容器的可写层,不影响镜像。docker compose up -d 重建容器时,容器*总是从镜像启动,cp 进去的文件不在镜像中,所以丢失。

教训

  • docker cp 不持久——它活在容器的生命周期内
  • 容器重建 = 回到镜像的初始状态
  • 以为"之前 cp 过了"就不需要管了,没意识到重建会丢
  • 更不应该的是:丢了一次,又 cp 回去,然后再次 up -d 又丢,反复了三次

正确做法

前端 docker cp 后,立即 docker commit 保存为新镜像:

# 前端部署完整流程
cd frontend && npm run build
scp -r dist/* root@server:/tmp/frontend-dist/
ssh root@server "docker cp /tmp/frontend-dist/. scilit-frontend-1:/usr/share/nginx/html/"
ssh root@server "docker commit scilit-frontend-1 scilit-frontend:latest"   # ← 必须

现在镜像和容器一致,随便重建都不丢。

如果后续要改 compose 文件(比如改 volume、port):

# 改 compose 前,先确保镜像是最新的
docker commit scilit-frontend-1 scilit-frontend:latest  # 或
docker compose build frontend                             # 重新构建

# 然后才改 compose → up -d

注意事项

  • 后端不需要 commit — backend 容器几乎从不重建,cp 一次就是永久的
  • 前端只要被重建过(包括 compose 版本升级、port/volume/env 改动),之前 cp 的就是白做的
  • 改 compose 文件 = 容器必然重建,改之前确认镜像已包含最新代码

问题三:Nginx 502 Bad Gateway

现象

  • Backend 容器重启后,所有通过 nginx 代理的 API 请求(/api/)都返回 502
  • 直接访问 backend127.0.0.1:8000)正常
  • Docker DNS 能解析 backend 主机名
  • 重启 frontend 容器后临时恢复

根因

Nginx proxy_pass http://backend:8000 使用固定字符串,nginx 只在启动时做一次 DNS 解析,把主机名 backend 解析为具体 IP。Backend 容器重启后 Docker 可能分配了新 IP,但 nginx 仍用旧 IP 去连,导致 502。

resolver 指令虽然加了,但只对变量形式的 proxy_pass 才触发动态解析。固定字符串 http://backend:8000 仍然启动时一次解析。

教训

  • 知道 resolver 的作用,但不知道它只对变量形式生效
  • 加 resolver 时感觉"解决了",实际上完全没生效——加了等于没加
  • 重启 frontend 容器能临时恢复(DNS 重新解析一次),所以以为好了

正确做法

两处要同时改:

resolver 127.0.0.11 ipv6=off valid=30s;

server {
    # 在 server 块定义变量
    set $backend_upstream http://backend:8000;

    location /api/ {
        proxy_pass $backend_upstream;     # 变量形式 → 每次请求都解析
    }

    location /api/v1/ws/ {
        proxy_pass $backend_upstream;     # 同样改
    }

    location /health {
        proxy_pass $backend_upstream;     # 同样改
    }
}
  • 必须同时有 resolver + proxy_pass $variable,缺一不可
  • 变量只在 server 块内定义,不能在 location 块内
  • nginx -t 验证语法通过后 reload 生效

注意事项

  • 固定字符串的 proxy_pass 只在启动/重载时解析一次
  • 变量形式 proxy_pass $var 每次请求都通过 resolver 重新解析
  • 但变量形式不支持某些特性(如 URI 重写),目前场景不需要
  • 验证方法:curl -s -w "%{http_code}" http://localhost:80/api/v1/health

问题四:Backend 和 Postgres 在不同 Docker 网络

现象

  • 容器启动后 health check 一直报 DB connection failed
  • alembic upgrade headName or service not known
  • docker network connect 到另一个网络后恢复正常

根因

生产 compose 文件定义了 networks: scilit,但 postgres 服务没有显式指定 networks:Docker 自动为它创建了默认的 scilit_default 网络。backend 在 scilit_scilit 网络。两个容器不在同一网络,无法通过主机名通信。

教训

  • compose 文件定义了网络但不一致,部署时没有验证所有服务在同一网络
  • 网络定义和 service.networks 的对应关系是分开的两段配置,容易漏

正确做法

每个服务显式指定网络:

services:
  postgres:
    networks:
      - scilit
  backend:
    networks:
      - scilit
  frontend:
    networks:
      - scilit

验证方法:

docker inspect <容器名> --format '{{json .NetworkSettings.Networks}}'
# 所有容器应该在同一个 network 下
# 或者:docker network inspect scilit

注意事项

  • 有多个 compose 文件时(docker-compose.ymldocker-compose.prod.yml),各自的 networks 定义互不影响
  • 如果一个问题表现为"容器间不通",先检查网络是否一致
  • dokcer compose up -d 启动新容器时不检查旧容器的网络归属

问题五:Refresh token 告警代码部署后消失

现象

  • docker cp auth.py 后验证代码在容器内 → grep consume_verify_token 显示 2 个匹配
  • docker restart 后再次验证 → 0 个匹配,旧文件

根因

之前 docker cp app/. 时 tar 解压多了嵌套路径 /root/scilit/backend/backend/app/,覆盖了"已有的"旧 /root/scilit/backend/app/ 吗?不,实际上第一次 cp 后容器内确实有代码,但 backend 容器后来被重新创建过(因为网络修复等操作),导致文件再次丢失。

教训

和问题二的根因一样:docker cp 不持久化。没有 commit,任何一次容器重建都会丢失。

正确做法

后端 docker cp 后验证了就行——后端容器几乎不会重建。但如果因其他原因容器被重建了,才需要重新 cp 或 commit。

注意事项

  • 后端容器的"几乎不会重建"不代表永远不会
  • 如果同时出多个问题时(像今天这样),容器可能反复重启/重建
  • 这种情况下后端也需要 commit,或者把所有改动合并在一次 cp 中完成

总结:今天所有问题的共同模式

问题 表面原因 深层原因
tar 嵌套 路径前缀不对 没有预览包结构就解压
cp 的文件被覆盖 容器重建 不知道/忘了 cp 不持久化
nginx 502 固定字符串不触发动态解析 知道一半,不知道另一半
网络不通 网络定义不一致 多 compose 文件的 network 归属
代码又丢了 同上(容器重建) 多重问题叠加

每个单独看都是小常识。但 5 个叠加就变成了"怎么什么都不行"的局面。

执行建议

当前部署方式是 SCP + docker cp,对整个团队(其实就你一个人)来说够用了。但每次部署前应该问自己两个问题:

  1. 我改的这些文件,后台容器是否会被重建? 如果会 → 需要先构建镜像或 commit
  2. 这步操作是不是在改变容器的运行时状态? 如果是 → 容器重建后这个状态不保留

快速排查清单(下次出问题时开查)

# 1. 容器到底是不是最新的版本?
docker diff <容器名>         # 可以看到文件系统改变了多少(cp 的文件)

# 2. 镜像包含什么?
docker run --rm <镜像名> <命令>  # 比如验证前端 dist/ 里的文件

# 3. 网络对吗?
docker network inspect <网络名>

# 4. tar 包里是什么?
tar tf 包名 | head -10

# 5. proxy 通了没?
curl -s -w " %{http_code}" http://localhost:80/api/v1/health