Files
ai-kefu-admin/docs/deploy-production.md
T
浅念 9315dba228 docs(deploy): 共享域名模式部署工件
模板:共享域名 server 块注释示例(kefu.example.com 占位,WS 升级头
落 regex location ^/t/[a-z0-9-]+/ws/——路径模式 WS 真实路径带 /t 前缀);
XFF 追加式语义注记(登录限流取末段,勿改覆盖式);--root-path 禁用
边界(root_path 前移 raw_path,路径前缀解析永不命中)。

手册:§3.6 共享域名模式(概念/零 DNS 接入流程/地址形态表/无尾斜杠
404 语义/部署注记三条);§6.5 加租户补共享域名零反代路径;1Panel
注记补 ws.conf regex 形态。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-19 19:44:46 +08:00

28 KiB
Raw Blame History

all-kefu 生产部署手册(含切换 runbook)

对应工件:Dockerfile(两阶段构建)、docker-compose.yml(name: all-kefu)、 deploy/openresty-all-kefu.conf.template(反代模板)、 scripts/{bootstrap_platform,import_legacy,archive,breach_rate}.py。 本文是 runbook 工件(Phase 4 交付),不是已执行实录——首次实际部署到阿里云前, 按用户全局部署 SOP(~/.claude/memory/general-deployment-sop.md + 全局 CLAUDE.md 规程) 与用户确认环境与流程后再动手。

手册继承 jc 项目 docs/deploy-production.md 的全部教训类条目(§7 踩坑实录逐条标注来源), 部署步骤按本项目实况重写:单容器多租户(jc 是每项目一容器),反代从"一站一容器" 变为"多域名同 upstream"。

0. 线上环境总览(目标形态)

项 值
目标服务器 阿里云 ECS 47.254.176.10(Debian + 1Panel,Docker 29.x 满足要求)
应用容器 all-kefu-app-1(宿主端口 8018:8018,内存限额 768m,--workers 1 硬约束)
租户域名 chat.agent.hnxinyouhe.com(auto)/ chat2.agent.hnxinyouhe.com(jiancai)——1Panel 各建一站,全部反代 → 127.0.0.1:8018
管理后台 形态 A(独立管理域名)或形态 B(租户域名下 /admin 路径分流),见 §3.3
代码位置 建议 /opt/all-kefu(compose 已钉项目名 all-kefu,目录名不影响项目名,与 1Panel 站点目录彻底解耦)
git 仓库 <repo-url>(占位——部署时替换;本仓编写时点未配置 remote)
持久化 ./data(platform.db + uploads + archives)、./tenants(每租户 <slug>.db)两个 bind mount,容器可随手重建

同服务器共存的老项目(切换期绝对不要删,回滚全靠它们):jc 项目容器 (chat2.agent.hnxinyouhe.com,端口 8017)、汽车客服容器(chat.agent.hnxinyouhe.com, 端口 8016)。切换 runbook 见 §5;观察期结束前老容器保留不删。

1. 环境与前置

1.1 服务器要求

  • Docker ≥ 24,Docker Compose ≥ 2.24。Compose 下限是硬要求:本项目 docker-compose.yml 的 env_file 用对象式写法(path: .env + required: false), 2.24 以下直接语法报错。验证:docker compose version。
  • 服务器无需 Python / pip / node——运行时全在镜像内(python:3.13-slim,admin 前端 在构建阶段一由 node:20-alpine 编译,服务器零 node)。pip install -r requirements.txt 只是本地开发的动作;本地必须装(含 python-multipart>=0.0.20——Phase 2 实证缺失 它头像上传直接 500,FastAPI multipart 解析依赖)。
  • 磁盘:日志已封顶(json-file 10m×3),主要增长项是 tenants/*.db 与 lead_push_logs.request_payload(只涨不掉,超 12 个月走归档 §6.3)。

1.2 代码获取

git clone <repo-url> /opt/all-kefu     # <repo-url> 部署时替换为真实仓库地址
cd /opt/all-kefu

1.3 生产 .env 逐项说明

放在仓库根 ./.env(compose 自动注入容器;chmod 600 .env)。缺省也能起 (出厂默认 + 启动日志 WARNING),但生产必须提供下面标"必设"的项:

# ---- 超管引导(bootstrap_platform.py 与应用 lifespan 同源消费)----
ADMIN_USERNAME=admin            # 可改;默认 admin
ADMIN_PASSWORD=<强密码>         # 必设!默认 admin123——启动检测到默认值会打
                                # WARNING 日志,且超管首登被强制改密(409 门)

# ---- AI(平台默认层;租户凭据空槽回退到这里)----
AI_PROVIDER=glm
ZHIPU_API_KEY=<智谱 Key>        # 必设(聊天引擎无 Key 不可用)
ZHIPU_BASE_URL=                 # 留空=SDK 默认地址;coding 套餐填
                                # https://open.bigmodel.cn/api/coding/paas/v4
AI_MODEL=glm-4-flash

# ---- 邮件(平台默认层;租户 config.email 空槽逐键回退)----
SMTP_HOST=<发件主机>
SMTP_PORT=465
SMTP_USER=<发件账号>
SMTP_PASS=<发件密码>
NOTIFY_EMAIL=<兜底询盘通知收件箱>   # 租户未配 notify_emails 时的回退

# ---- 询盘推送 UniService(平台默认层;租户 config.push 空槽回退)----
PUSH_ENABLED=false
PUSH_API_URL=https://uniservice.server.hnxinyouhe.com/open/v1/inquiry/push
PUSH_API_KEY=<开启时用>
PUSH_EXTERNAL_KEY_PREFIX=ai-kefu-jc   # 平台兜底前缀;两租户已各自配
                                      # ai-kefu(auto)/ ai-kefu-jc(jiancai),
                                      # 与各自老项目线索键**同键空间**(切换/
                                      # 回滚不产生重复线索的依据),勿改;
                                      # 两租户之间互异即可
# ⚠ 推送探测 env 回退(已知项,CLAUDE.md Phase 4 清理单 #2):租户 push URL
#   空槽时回退 env PUSH_API_URL(默认即真 UniService 生产址)——后台凭据页点
#   「探测」会对真实服务发空批量 POST({"messages": []},按契约应为无操作,
#   但属真实出站)。介意则把 env PUSH_API_URL 设为 stub 域名再探测。

# ---- 租户源 ----
TENANT_SOURCE=db                 # 默认即 db(生产装配:platform.db 注册表);
                                 # file = 降级读 seed_profiles 目录(冷备/本地
                                 # 调试形态,线上非应急勿用)

# ---- 说明 ----
# APP_HOST/APP_PORT 在容器内不生效(Dockerfile CMD 已钉 0.0.0.0:8018);
# APP_DEBUG 当前无消费方(摆设,写了也不起作用);TZ=Asia/Shanghai 由
# compose environment 固定注入,勿动。

.env 与 data/、tenants/ 都不在 git 里(.gitignore 覆盖),发版更新不受影响。

2. 首次部署流程

cd /opt/all-kefu

# 1. 镜像(两阶段构建:node 编译 admin → slim 只带运行时;首次约数分钟)
docker compose build

# 2. 平台引导:建 platform.db schema + 超管账号 + 导入出厂租户(auto + jiancai)
#    幂等可复跑:超管空表才建、已存在 slug 的租户跳过
docker compose run --rm app python scripts/bootstrap_platform.py

# 3. 起容器
docker compose up -d

# 4. 健康检查(分级 JSON,解读见下)
curl -s http://127.0.0.1:8018/health

bootstrap 输出应含:platform.db: /app/data/platform.db、超管用户名、 auto: 导入(...)、jiancai: 导入(...)(复跑则显示"跳过(tenants 已存在)")。 platform.db 落在 data/ bind mount 内(宿主视角 /opt/all-kefu/data/platform.db), 容器重建不丢——这是 P4T1 审查修复项,compose 已落实,勿把库挪回仓库根。

/health 分级解读:

{
  "status": "ok",            // 任一已建租户库探活失败 → "degraded"(仍 200,
                             //   单租户库损坏不杀容器、不影响其余租户)
  "tenants": [
    {"slug": "auto",     "status": "active", "engine_ok": null},
    {"slug": "jiancai",  "status": "active", "engine_ok": true}
  ],
  "domains": 3              // 域名绑定总数:auto 1 + jiancai 2
                             //   (chat2.agent.hnxinyouhe.com + localhost.jc,出厂档案如此)
}
  • engine_ok: true = 探活通过;false = 该租户库损坏(排查该租户的 db); null = engine 懒建未就绪——健康检查不触发建库(设计如此),该租户 来过一次请求后才会变 true,刚启动全是 null 属正常。
  • domains 是导入完整性的人工核对口径:bootstrap 后应为 3(含 localhost.jc)。

浏览器打开 https://<admin 地址>/admin/(反代未配时先见 §3;也可临时 ssh -L 8018:127.0.0.1:8018 隧道 + http://127.0.0.1:8018/admin/),用 .env 的超管账号首登——强制改密(must_change_password 409 门,改完才进)。 租户管理员账号不在 bootstrap 里建:后台"租户管理 → 创建向导/详情"按需补。

3. 反代配置(1Panel)

模板:deploy/openresty-all-kefu.conf.template(完整可抄 + 1Panel 适配注记)。 要点速览:

3.1 租户域名 → 127.0.0.1:8018

每租户域名一个 1Panel 站点(chat. / chat2. 各一站),upstream 全指向 127.0.0.1:8018——多域名同容器,这是与 jc 时代"一站一容器"的最大差异。 站点片段放 /opt/1panel/www/sites/<域名>/proxy/{root,ws}.conf。

3.2 必带的三件事(每站)

  1. proxy_set_header Host $host——本项目按 Host 头解析租户,漏了全站静默 404。
  2. WS 升级头单套(location /ws/:Upgrade/Connection + http 1.1 + 3600s)—— 1Panel 面板生成的反代若自带一套 Upgrade/Connection,删面板那套,重复两套 = uvicorn 拒绝握手(HTTP 正常、WS 426,jc 线上事故)。
  3. client_max_body_size 5m——头像上传上限 2MB,nginx 默认 1m 直接 413。

另外两条不改配置只做核对:/static/widget.js 响应应带 Cache-Control: no-cache (应用层中间件已下发,反代别加会覆盖它的 expires);/api/public/* 放行 (widget 头像/品牌端点,按租户 Host 解析——全站反代天然覆盖,按路径收紧配置时 漏掉它的症状是 widget 头像 404)。

3.3 管理后台两形态(二选一)

  • 形态 A:独立管理域名。1Panel 为 <admin 域名>(部署前由需求方确定)建站, 整域反代 8018。/admin(SPA)与 /api/platform(后台 API)同容器同 upstream; 入口是 https://<admin 域名>/admin/——管理域根路径 / 走租户 Host 解析, 404 属预期(可在 1Panel 站点配个跳转)。 已知边界:管理域下 /api/public/* 必 404(该前缀按租户 Host 解析,管理域是 未知 Host)——后台"品牌-头像预览"加载不出,已知项不是故障(清理单 #3), 预览以页内本地简化预览为准。
  • 形态 B:路径分流。不建独立域名,在(任一)租户域名站点下加 location /admin 与 location /api/platform/ 两块指向 8018——应用层对这两个 前缀豁免租户 Host 解析,挂在租户域名下照常工作,同源零 CORS。已知边界:头像 预览走本域 /api/public/avatar,解析到本域名对应的租户,跨租户切换配置页 时预览不准(清理单 #3/#4)。

3.4 主 conf include 行检查(1Panel 坑)

/opt/1panel/www/conf.d/<域名>.conf 必须含 include /www/sites/<域名>/proxy/*.conf;——1Panel 建站默认可能不带,面板配了 反代却不生效九成是这行缺失。手工补前备份同目录 *.conf.bak.<日期>;改完 nginx -T 可 dump 最终生效配置对照。

3.5 验证与重载

# 先 -t 再 reload(reload 平滑,不影响其他站点)
docker exec $(docker ps --filter name=openresty -q | head -1) nginx -t \
  && docker exec $(docker ps --filter name=openresty -q | head -1) nginx -s reload

# 逐域名验证
curl -s https://chat2.agent.hnxinyouhe.com/health            # {"status":"ok",...}
curl -sI https://chat2.agent.hnxinyouhe.com/static/widget.js | grep -i cache
curl -s -i --http1.1 -H "Connection: Upgrade" -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" -H "Sec-WebSocket-Version: 13" \
  https://chat2.agent.hnxinyouhe.com/ws/chat | head -1        # 期待 HTTP/1.1 101

3.6 共享域名模式(单域名多租户,可选)

设计出处:spec docs/superpowers/specs/2026-10-19-shared-domain-routing-design.md。

概念:一个共享域名(如 kefu.hnxinyouhe.com)承载任意多租户——租户标识 在 URL 路径前缀 /t/<租户slug>/ 中,与 §3.1 "每租户一域名"的 Host 模式 共存,解析优先级:路径前缀 > Host(/t/<slug>/... 命中即剥前缀按 slug 解析租户;不带前缀的请求照走 Host 解析——共享域名自身不是租户域名, 根路径 404 属预期)。

接入流程(零 DNS 操作):

  1. 平台设置(仅超管)配 shared_widget_domain = 共享域名(运营填一次);
  2. 建租户——域名可整组留空(纯路径模式租户,创建向导支持跳过域名);
  3. 配置中心 → 品牌与外观 → 嵌入代码 → 复制「共享域名形态」→ 贴到目标网站。

地址形态:

项 形态
聊天页 https://<共享域名>/t/<slug>/
挂件脚本 https://<共享域名>/t/<slug>/static/widget.js(脚本地址即租户标识,无需新参数)
data-site 参数 照常可用;缺省取租户首个站点

无尾斜杠语义:/t/<slug>(无 /)→ 404——仅带尾斜杠形态才剥离 前缀(spec §3 防歧义设计,不做 308 重定向);嵌入代码生成的就是带斜杠正确 形态,勿手工"修正"。

部署注记:

  • 共享域名 = 一条反代(1Panel 一站),§3.2 三件事同款:透传 Host 头、 WS 升级头单套、client_max_body_size 5m;WS 超时 3600s 同既有。模板 deploy/openresty-all-kefu.conf.template 有现成注释示例块(kefu.example.com 占位)。WS 位置差异:路径模式 WS 的真实路径是 /t/<slug>/ws/chat—— 升级头必须落在 regex location ~ ^/t/[a-z0-9-]+/ws/(照抄模板共享域名块; 1Panel 同样放 proxy/ws.conf),只配 location /ws/ 匹配不到它(症状: 聊天页能开、WS 连不上)。
  • uvicorn 不得加 --root-path:路径前缀解析在 raw_path 上做,root_path 会前移 raw_path → /t/ 前缀永不命中,全站退化为 Host 解析 404(同模板注记)。
  • 登录限流取 XFF 末段(app/platform/routes/auth_routes.py _client_ip) ——依赖反代 $proxy_add_x_forwarded_for 的追加语义(客户端自带在前、 真实来源追加在尾);勿改成 X-Forwarded-For $remote_addr 覆盖式(同模板注记)。

4. 存量迁移(老 jc / auto 项目 → 本平台)

前提:老项目与本项目同机(47.254.176.10);若跨机,先把导出文件 scp 到 /opt/all-kefu/data/ 下(compose 已挂载该目录,容器内路径 /app/data/; 注意宿主 /tmp 默认不在容器挂载内,docker compose run 看不到宿主 /tmp 的文件)。

4.1 老库导出(二选一,禁运行时裸 cp)

# 方式一(最稳):停老容器再拷
docker stop hs-ai-kefu    # jc 容器名,以老项目实录为准
cp /opt/1panel/www/sites/chat2.agent.hnxinyouhe.com/index/data/hesupipe_ai_kefu.db \
   /opt/all-kefu/data/legacy-jc.db

# 方式二(不停机):SQLite backup API / VACUUM INTO 导一致性快照
#(老容器内库路径以 jc 实录为准:WORKDIR /app + ./data/hesupipe_ai_kefu.db)
docker exec hs-ai-kefu python -c \
  "import sqlite3; c=sqlite3.connect('/app/data/hesupipe_ai_kefu.db'); \
   c.execute(\"VACUUM INTO '/app/data/export-jc.db'\"); c.close()"
cp /opt/1panel/www/sites/chat2.agent.hnxinyouhe.com/index/data/export-jc.db \
   /opt/all-kefu/data/legacy-jc.db

禁止运行时直接 cp 活库文件:SQLite WAL 模式下可能拷到写一半状态。 停容器或 backup API / VACUUM INTO,没有第三条路。auto 项目同理(库名/容器名 以汽车项目服务器实录为准)。

4.2 导入 + 对账

cd /opt/all-kefu
# 租户须已 bootstrap(§2 已导入 auto/jiancai 档案,门禁自会校验 platform.db)
docker compose run --rm app python scripts/import_legacy.py \
  --source /app/data/legacy-jc.db --tenant jiancai
# auto 项目同理:
docker compose run --rm app python scripts/import_legacy.py \
  --source /app/data/legacy-auto.db --tenant auto

脚本纪律:老库只读打开(mode=ro,绝不回写);保 ID/session_id/status/ page_token 原样(external_key 连续,UniService 不产生重复线索);切换前 pending 推送日志转 failed 并追加备注;幂等(INSERT OR IGNORE,幂等键=主键, 重跑零变化)。

对账报告(stdout)逐项核对:

  • 每表行数四元组:源/导入/跳过已存在/目标合计,源=目标才算一致;
  • 最近 10 条会话抽查:id/session/status 逐条对照;
  • 各子表 MAX(conversation_id) 对照(ID 原样性);
  • pending→failed 转换条数;
  • 列差异标注(老库缺列→默认值填充、老库多列→不迁移)——确认无意外项。

任一不一致 → 脚本非零退出,人工核查清楚之前不得切换域名。

5. 切换 runbook(独立大节——按序执行,每阶段有出口)

原则:域名最后切。切换 = 只改 1Panel 反代 upstream(老容器端口 → 127.0.0.1:8018),DNS 不动、老项目不删。

阶段一:验证(不切域名)

新平台已部署(§2)+ 已迁移(§4)+ 反代片段已备好但未指流量。全功能验证 走"隧道 + hosts",本机把租户域名锚到新平台:

# 本机(运维机)hosts 加:127.0.0.1 chat.agent.hnxinyouhe.com chat2.agent.hnxinyouhe.com
ssh -L 8018:127.0.0.1:8018 root@47.254.176.10    # 隧道保持
# 浏览器访问 http://chat2.agent.hnxinyouhe.com:8018/ 即为新平台(Host 命中租户)

验证清单(逐项过,全绿才进阶段二):

  • /health:status=ok、两租户 active、domains=3
  • 登录后台 /admin/:超管登录、强制改密流、租户管理员登录(后台建号)
  • 两租户 widget 冒烟:http://chat<...>:8018/static/widget-test.html 邀请卡弹出、面板展开、发一句消息收到 AI 回复(真实 ZHIPU_API_KEY)
  • WS 握手 101(上方 curl 法)+ 面板长连不掉
  • 配置中心:品牌/词表加载、版本历史(bootstrap 写入的版本 1 可见)、试金石跑一轮
  • 统计:迁移数据可见(会话数与老平台对得上)、推送健康表渲染
  • /api/public/avatar 200(头像)、/static/widget.js 带 no-cache
  • 后台数据视图:翻到迁移导入的最近会话,消息流完整

阶段二:切反代(逐域名)

1Panel 逐站点把反代 upstream 从老容器端口改为 127.0.0.1:8018 (一个域名一个站点;先切一个租户域名,观察半天再切第二个)。每切一个: nginx -t → reload → 立即线上验证 /health、widget 加载、WS 101、一条真实 留言走通(AI 回复 + 询盘邮件到达)。

阶段三:观察一周(日检清单)

  • /health 无 degraded(有 = 对应租户库问题,立刻查)
  • 后台"统计 → 推送健康"成功率正常(与切换前老平台口径对比)
  • 询盘邮件正常到达(单邮件通道,SMTP 失败不自动补发,头几天盯收件)
  • breach_rate 双向偏差观察(沿用 jc 观察法):脚本已随迁至本仓 scripts/breach_rate.py(多租户适配:--tenant 必填,经 platform.db 门禁后只读打开 tenants/<slug>.db,默认路径与容器挂载对齐,零额外参数): bash docker compose run --rm app python scripts/breach_rate.py --tenant jiancai # 容器已起时也可 exec 形态:docker compose exec app python scripts/breach_rate.py --tenant jiancai (漏放/误杀残差与 jc 期基线对比;误杀侧不在脚本口径,用后台会话详情人工复核)
  • 磁盘与日志:df -h、docker stats all-kefu-app-1 --no-stream (内存应显著低于 768m 限额)、docker logs all-kefu-app-1 --tail 100

阶段四:老项目停用归档(观察期满且无回滚诉求)

老容器 docker stop(不 rm)、镜像留存;老库文件归档备份至备份盘; 1Panel 老站点配置保留(回滚素材)。至此切换闭环。

回滚预案(任一阶段发现不可接受问题)

反代切回老容器 = 该域名 1Panel 站点 upstream 改回老端口(8016/8017)→ nginx -t → reload,约 30 秒。老项目容器/数据在观察期内原样保留,这就是 不删它们的原因。注意:切换后新平台产生的会话在回滚后会留在新平台库(老平台 没有)——属预期,如实告知需求方;UniService 线索不会重复(conversation_id 保原样 + external_key 前缀不变)。

6. 日常运维

6.1 备份(每日;platform.db 按敏感件对待——内含 SMTP/push/AI 密钥)

用 SQLite backup API 做一致性快照(WAL 安全),禁运行时直拷 .db:

cd /opt/all-kefu
docker compose exec -T app python - <<'PY'
import sqlite3, pathlib, datetime
stamp = datetime.date.today().strftime('%Y%m%d')
targets = [pathlib.Path('/app/data/platform.db'),
           *sorted(pathlib.Path('/app/tenants').glob('*.db'))]
for p in targets:
    dst = f'/app/data/backups/{stamp}-{p.name}'
    pathlib.Path(dst).parent.mkdir(parents=True, exist_ok=True)
    src, out = sqlite3.connect(p), sqlite3.connect(dst)
    src.backup(out); out.close(); src.close(); print(dst)
PY
# 备份落在宿主 data/backups/(同盘!)——必须再 rsync/scp 出服务器才算备份

cron 示例:把上面整段 heredoc 存成 /root/all-kefu-backup.sh,cron 挂 10 4 * * * /root/all-kefu-backup.sh(快照后再由 rsync/scp 任务拉走,04:30 前后错开)。 季度恢复演练(没验证过的备份等于没有备份):取一份历史备份,起临时容器验 PRAGMA integrity_check + 关键表行数,与线上对得上才归档:

docker compose run --rm -v /opt/backups/all-kefu/20260401-platform.db:/tmp/r.db:ro \
  app python -c "import sqlite3; c=sqlite3.connect('/tmp/r.db'); \
print(c.execute('PRAGMA integrity_check').fetchone()); \
print(c.execute('SELECT count(*) FROM tenants').fetchone())"

6.2 日志与 PII 现状

  • 封顶已配:json-file max-size 10m × max-file 3(Docker 默认不限大小会涨满磁盘, compose 已落实)。看日志:docker logs all-kefu-app-1 --tail 100。
  • PII 现状(如实记录):spec §9 承诺"不打聊天内容等 PII",但当前引擎层 日志含会话内容摘录(conversation.py 若干 logger.info/warning 带消息片段 %r 截断输出);平台层(app/platform/)零日志、不落凭据。整改方向:观察期内 收口(摘录降级为 session_id 索引)——在此之前,日志文件按含 PII 对待, 备份/外发时注意。
  • 登录限流现状:反代形态下所有限流键同源(反代 IP),退化为按用户名限流 (fail-safe 只更严不更松);XFF 可信解析归后续批次。

6.3 数据归档(archive.py)

lead_push_logs.request_payload 存完整对话 JSON 只涨不掉;会话超 12 个月 走归档(导出 JSON → 删行 → VACUUM 回收):

docker compose run --rm app python scripts/archive.py --tenant jiancai --dry-run  # 先只列不动
docker compose run --rm app python scripts/archive.py --tenant jiancai           # 实际执行

导出落 data/archives/<tenant>/<时间戳>-365d.json(先落盘后删库,崩溃级安全); 对账不一致即非零退出。维护窗口建议:低峰(凌晨)执行——VACUUM 需独占, 被占用锁时脚本自动跳过并提示稍后手动补跑;导出/删除本身单事务安全。

6.4 升级发版与回滚

# 本地:全量测试全绿才发版(后端 pytest + 前端 vitest/build),然后 push
git push origin <branch>

# 服务器:
cd /opt/all-kefu && git pull
docker compose build          # 静态/前端改动必须 build(打进镜像,仅 up -d 不够)
docker compose up -d
curl -s http://127.0.0.1:8018/health

版本 tag 纪律(30 秒回滚的来源):发版时在 compose 的 app 服务补/改 image: all-kefu:<版本tag>(与 build: . 并存——build 产物自动打上该 tag), up -d 即切新版;回滚 = image 改回上一 tag 再 up -d(旧镜像仍在本地, 30 秒内完成)。当前 compose 尚未带 image 字段(首发后落地此纪律)。

6.5 加租户(零容器操作)

加租户 = 后台建租户 + 1Panel 反代加一条,不起容器: 后台"租户管理 → 创建向导"(消费出厂档案/空档案)→ 站点绑定新域名 → 1Panel 为新域名建站反代 127.0.0.1:8018(§3 同款三件事)→ 完成。同一容器自动承载。 已启用共享域名(§3.6)时更省:新租户零域名、零 DNS、零反代操作——直接按 /t/<slug>/ 形态取嵌入代码即完成接入。

6.6 bootstrap 复跑

docker compose run --rm app python scripts/bootstrap_platform.py 幂等: 已存在 slug 的租户跳过、超管空表才建——发版后跑一遍无副作用(也可不跑, lifespan 启动同样保证 schema/超管就绪,两者同一套幂等实现)。 注意:容器运行中跑 bootstrap 不会热刷新内存租户注册表——/health 仍显示旧租户列表,重启容器(docker compose restart app)后才反映; 首次部署按 §2 顺序(bootstrap 先于 up)则无此问题。

7. 踩坑实录(排查手册——每条都是真事故/真审查发现)

# 现象 根因 解法(已内置/纪律) 来源
1 同机两个客服项目起一个、另一个 502 1Panel 站点目录都叫 index,compose 默认以目录名为项目名 → 互删容器 compose 钉死 name: all-kefu(根治);且本机只剩这一个 compose 项目 jc 线上事故
2 改了 widget.js / admin 前端,线上"没生效" 静态与前端 dist 打进镜像,up -d 不重建镜像 发版必须 docker compose build;/static/* 已由应用层下发 no-cache(用户端 Ctrl+F5 一次即愈) jc 线上事故
3 1Panel 面板配了反代不生效 面板建站主 conf 默认可能不含 include proxy/*.conf 手工补 include 行(§3.4),备份后 nginx -T 验证 jc 线上事故
4 WS 握手 426(HTTP 正常) 反代里 Upgrade/Connection 头重复两套,uvicorn 严格解析拒绝 升级头全局只留一套(§3.2);nginx -T dump 最终配置定位 jc 线上事故
5 bottom-left 弹出元素溢出屏幕 widget 弹出层 CSS 硬编码只适配右下角 已随 pos-<position> 类联动;新增弹出元素必须同时加联动规则(静态测试锁死) jc 线上事故
6 mem_limit 为什么是 768m 单进程(--workers 1 硬约束)承载 admin SPA 服务 + 全部租户引擎,较 jc 单租户 512m 上调 compose 已配;改前先看 docker stats 实测 本项目
7 容器内时间不对/推送窗口漂移 基镜像缺 tzdata 时 TZ 被静默忽略(退 UTC) compose 固定 TZ=Asia/Shanghai + Dockerfile 显式装 tzdata(基镜像漂移保险);仪表盘"今日"边界依赖它 本项目
8 widget 头像 404 / 后台头像预览空 /api/public/* 按租户 Host 解析:反代按路径收紧时漏配它;admin 独立域名(形态 A)下必 404 租户域名站确保全站反代覆盖 /api/public;形态 A 预览缺失是已知项(清理单 #3)非故障 本项目
9 platform.db 随容器重建丢失 库曾落仓库根(非卷内) 已修:PLATFORM_DB 钉 data/platform.db(compose bind mount),勿挪 本项目(P4T1 审查)
10 healthcheck 起不来容器 slim 镜像无 curl compose healthcheck 用容器内自带 python urllib 探 /health 本项目
11 admin 页白屏 dist 挂载在 /admin 前缀,资源引用前缀不一致 VITE_BASE_URL=/admin/ 钉在 admin/.env.production(单一事实源,不靠构建期 env 注入);动它必须重新 build 本项目
12 反代配好后全站 404(非报错) 租户路由按 Host 头解析,反代漏 proxy_set_header Host $host → upstream 收到的是 127.0.0.1 这类未知 Host 反代三件事之一(§3.2);排障先 curl 直打 8018 带 -H "Host: <租户域名>" 对比 本项目新增预判
13 头像上传 413 nginx 默认 client_max_body_size 1m < 应用上限 2MB 反代层放 5m(§3.2) 本项目新增预判

附:与 jc 手册的继承关系

  • 继承(教训类,全部进 §7 + §3/§6 相应小节):compose 撞名互删、静态打进 镜像、1Panel include 行、WS 426、widget 缓存、bottom-left 联动、备份禁裸拷、 日志封顶、WS/HTTP 分离的超时策略。
  • 不继承(部署形态不同,已按本项目重写):一站一容器 → 单容器多租户多域名; deploy.sh 一键脚本 → 未提供(首次部署按 §2 手工,稳定后可自行固化); jc 的 /admin 单租户后台 → 本项目 /admin/ 多租户 SPA + 两反代形态。
  • jc 手册原文:老仓 D:\refactor\ai-kefu-jc\docs\deploy-production.md(服务器 布局/跳板链路/凭据来源等实况细节需要时回查)。