第二季第 1 篇(续篇从 101 重新起号)。拆解对象:
deploy/目录------仓库里唯一"给人用"而不是"给机器用"的部分。这篇不要求你懂 Docker、K8s 或运维,用到的概念都会在用到的地方当场讲。老规矩:只读不改两个仓库,文末照旧有一个跑通的 demo。
先交代这篇要拆的东西的真实状态------这比什么都重要。
DeepFlux 现在真实跑在生产上的,是自家云上的一套裸机部署,有真实的生产运维手册和事故记录(第九节讲)。仓库里另有一套完整的私有化交付工程:单机安装、离线安装包、升级、回退、备份、恢复,全部建好了。
但它还没有在任何一个真实客户的机房里完成过安装 。仓库自己的演练日志(docs/deployment/DRILL_LOG.md)开头写着一段状态校正(2026-08-11):
本文件中的 2026-06 条目和 native supervisord 段落是历史演练/预案记录,不构成当前首家客户验收证据 。......native、Helm、airgap 和 Linux clean-VM 仍须另行完成并留存外部演练证据。
连"在一台干净的 Linux 虚拟机上完整装一遍"都还没做过------已有的安装演练,是在开发者的 MacBook 上用 Docker 模拟干净环境跑的。
这不是这套工程的污点,反而是这篇要拆的重点之一:它把"验证到哪一步"标得清清楚楚。真跑过的、演练过的、从未跑过的,三层不混。这篇就按这个诚实的分层来走:它要解决什么问题、建成了什么、验证到哪、没验证的怎么标注。
一、先弄懂三个词
私有化部署:软件跑在客户自己的服务器上,数据不出客户的机房。对应的模式叫"SaaS"------跑在厂商云上、开网页就能用。
airgap(气隙隔离) :客户内网和互联网之间物理断开,中间隔着一道"空气墙",连网线都不存在。软件要进这种机房,没法在线下载,只能拷进 U 盘或移动硬盘,人工搬进去。所以做 airgap 交付,等于要做"全程不联网也能装好"的安装包。
Docker 镜像:现代软件交付的基本单位。它把"程序 + 运行所需的全部环境"打成一个压缩包,任何装了 Docker 的机器都能原样跑起来。可以想成"自带厨具和食材的半成品菜",到哪口锅都能加热。下文的安装包,主要内容就是几个镜像文件。
二、走错的那条路:先上 K8s,再砍掉
deploy/airgap/MANIFEST.md(交付物清单文档)开头,是一段自我否定的声明:
⚠️ 已冻结(2026-07-29) :airgap 的 k8s/helm 路线不再作为交付物。
被冻结的第一代设计长这样:一个约 3GB 的压缩包,里面是 6 个服务镜像、一份 Helm 安装配置、几块 Grafana 监控面板,外加一个前置检查脚本 preflight.sh。这个脚本逐项检查:kubectl 版本 ≥ 1.28、helm 已装、CPU ≥ 12 核、内存 ≥ 24GB、有 StorageClass、装了 cert-manager......
翻译一下。k8s(Kubernetes)是管理一整片服务器的"集群操作系统",Helm 是它的应用商店,kubectl 是它的遥控器,StorageClass 和 cert-manager 是它上面的存储、证书插件。那一长串检查翻译成人话是同一句:客户得先有一个由专人维护的 K8s 集群,才轮得到装你的软件。
门槛就出在这。要把系统装进自己机房的客户,往往恰恰没有这样一支运维团队。安装要求每多一条,客户的实施工程师就多卡一天。
于是有了第二代,前提条件缩成一句话:一台干净的 4 核 8G Linux 服务器,装了 Docker。 编排用 Docker Compose------一份配置文件描述全部服务、一条命令全部启动。文档定的目标是 30 分钟装完。
注意措辞:是目标 。docs/deployment/DELIVERY.md 原话:"clean-VM 实测完成前不视为客户验收结论"。从"要有 K8s 集群"降到"有台装了 Docker 的机器",方向是真的;30 分钟能不能做到,还没实测过。
三、U 盘里的包裹装什么,凭什么信它
离线安装的核心是一个自包含的压缩包:build-offline-bundle.sh 负责打包,load-offline-bundle.sh 负责装载。包里计划装:
- 镜像:平台本体 df_server;数据库 PostgreSQL(用带 pgvector 向量扩展的特别版------平台的知识库要在数据库里存向量,普通版连建表都过不去);缓存 Redis。客户要知识库功能,再加对象存储 Garage 和病毒扫描 ClamAV;
- 全套脚本:安装、升级、回退、备份;
- 可选:一个本地向量模型(ONNX 格式)------知识库检索靠它把文字变成向量,离线机房下载不了,必须随包带走。
打包脚本有两个值得学的细节。
白名单拷贝:只复制明确点名的十几个文件,注释原话"只复制白名单,不把本地 .env、诊断包或其它运行产物带进离线包"。构建机上可能躺着测试密钥------白名单保证它们不可能混进包。
禁 latest:Docker 镜像默认有个标签叫 latest("最新版")。四个生命周期脚本里都写死了"TAG=latest 被禁"。因为 latest 是个会漂移的名字:今天拉的 latest 和明天拉的,可能根本不是同一个东西。离线交付必须钉死版本号。
然后是信任问题:包从你手里到客户机房,中间要过销售、快递、好几台电脑,怎么确定没被动过手脚?古老的办法是封条。数字世界的封条叫 sha256 ------给每个文件算一串 64 位"指纹",内容哪怕改一个字节,指纹就面目全非。打包时给每个文件算好指纹,排成清单 manifest.sha256 放进包里;装载时做三道检查:
- 查路径 :不许有绝对路径(如
/etc/passwd)或../(能跳出解包目录),防止恶意包把文件写到系统目录; - 查类型 :只许普通文件和目录,不许符号链接------符号链接是"快捷方式",能指向包裹外的任意文件,是压缩包攻击的经典通道;
- 对指纹:逐个文件重算 sha256,一个字节都不能差。
这不是纸面设计。我把三道检查的逻辑原样抄出来写了个 demo(/tmp/e101demo/run.sh),构造四种包裹实测:
| 场景 | 结果 |
|---|---|
| 正常包裹(3 个文件) | ✓ 全部通过 |
| 改 1 个字节后重新打包 | ✗ 拒绝:校验和不匹配 |
| 塞一个指向 /etc/passwd 的符号链接 | ✗ 拒绝:特殊条目 |
| 清单里写绝对路径 /etc/shadow | ✗ 拒绝:路径非法 |
四场景全部按预期拦下------这个 demo 是本篇写作时在本机真实跑过的,它验证的是三道检查的逻辑本身;至于"整个 bundle 在真实客户机上装载成功",仍然没发生过,脚本注释自己写着:"完成实际客户机 load→install E2E 后,才可把某个 bundle 宣称为正式 airgap 交付物。"
四、验证到哪一步:一笔真实的账
这套工程最好的部分,是它对"验证"的记账方式。演练日志的编写原则原话:"只记真实执行过的演练,每条有日期、环境、结果与产出 commit。未演练的项目明确标注。"逐层盘点:
层一:真跑过的。 2026-06-10 有两场演练(都在 macOS 本机):
- 黄金路径演练 :16 步 curl 脚本从"gateway 无法启动"修到全绿。这场演练的价值在它揭出来的老底------当场修复 8 组问题,好几条的表述是"从未":session 存储的连接配置写错了地址格式,"从未连上";登录模块查的字段名和真实数据库对不上,"登录在真库从未通过"。在这次演练之前,整条端到端链路就没有被真实启动过。
- 干净环境安装演练 :把容器、数据卷、配置全删掉重来,第二轮 9.3 秒零人工干预 跑通(建表 → 初始化租户 → 健康检查通过 → 登录 → 建会话)。顺带实锤:被替换的旧安装脚本引用了 5 个从未被构建过的镜像、配着代码从未读取过的环境变量------旧安装路径从未可用。
另外,备份/恢复脚本有一套自动化门禁测试(用假的 docker/age 命令验证脚本在缺关键材料时会拒绝执行、不碰真实数据);全部 56 个部署脚本语法检查通过;以及上面我的信任链 demo。
层二:只在本机演练过、没上过真机的。 完整的装机流程、知识库的三个坑(第七节)------它们来自 macOS + Docker 的演练,不是客户机房。
层三:从未执行过的。 真实客户机安装;干净的 Linux 虚拟机安装(演练日志点名"仍须另行完成");离线包的端到端装载;升级和回退的实战;备份的真实加密与真实异地上传------连门禁测试的注释都写着"此处不宣称真实加密或远端可用性"。
三层加起来才是事实的全部。把"建成"说成"能交付",中间隔着的正是这三层。
五、装机的编排是怎么设计的
虽然没上过战场,这套安装的设计本身值得拆。install.sh 把装机编排成五步:
第一步:起数据库和缓存。 数据库用 pgvector 特别版镜像,原因前面说过。
第二步:先建表,后开机。 仓库的"红线 #5"。配置里有一个一次性的"迁移"服务:执行 df_cli migrate up 把数据库表结构一路升到最新(目前 145 个建表/改表语句),跑完就退出;主服务的启动条件里写着等迁移成功退出,才允许启动。为什么执着?新代码去查一张不存在的表,崩得毫无体面。
这里分了两把钥匙:建表用数据库超级用户账号(建表需要最高权限);主服务平时用非特权账号 app_user 连库。平台的租户数据隔离靠的就是后者------数据库的"行级安全"策略(每行数据带租户标记,越权读取在数据库这层被拦下)管的是普通账号,超级用户天然在约束之外。安全机制成立的前提,是平时跑业务不用高权限账号。
第三步:等健康检查。 轮询 /healthz 接口,最多 120 秒。
第四步:初始化租户。 这是黄金路径演练翻出来的坑:登录组件只会"见到新用户就顺手建账号",但前提是租户已存在,而全新的库里没有租户------所以全新安装会卡死在第一次登录。修复就是安装末尾固定跑一步 df_cli bootstrap(建默认租户),幂等,重复执行无害。
第五步:体检。 末尾强制跑 post-install-check.sh:容器在不在、进程活不活(/healthz)、数据库/缓存/知识库组件就绪没(/readyz)、磁盘内存够不够......逐项打 PASS / WARN / FAIL,支持 --json 输出,注释写明用途"便于客户自动化留证"。
最关键的纪律在最后:版本号只在体检通过后才写入磁盘,写法是先写临时文件再一步改名------进程中途被杀,也只有"有完整版本号"或"没有"两种结果。体检不过,这次安装就当没发生过。
六、升级和回退的设计
升级脚本(upgrade.sh)的顺序:先下载新版本镜像,下载成功,才改配置里的版本号。 反过来想就明白:先改配置再下载、下载又失败,机器就处于"配置写着新版、跑的是旧版",下次重启说不清。
从改配置那一刻起进入危险区,防护三件套:配置文件、当前版本号、上一版本号先全部备份 ;挂一个"无论脚本怎么退出都会执行"的善后钩子(shell 的 trap);新版本没跑起来就把三个文件原样恢复。git 历史里有两笔提交------"保证单机版本生命周期状态一致""强制生命周期执行安装后自检"------每条守护都是某次真实翻车刻出来的。
回退(rollback.sh)更保守,脚本开头印着丑话:
⚠️ 仅回退代码镜像,不动 DB schema。
翻译:代码能换回旧版,数据库的表结构回不去。 新版本若已把表改了,旧代码跑在新表上可能出错------得用恢复脚本把升级前的数据库整个倒回去。同样注意:这套升级/回退目前只有设计加演练,没有实战记录。
七、演练实锤的三个坑:知识库
知识库功能是"装完 ≠ 能用"的重灾区。2026-07-29 的 KB 演练(T8,macOS 本机)实锤了三个坑,全部记进了脚本注释:
坑一:文件不进数据库。 知识库的文档本体存在"对象存储"里(专门存文件的服务,随包带开源的 Garage)。而 Garage 装好后必须执行一次"布局分配"命令,否则节点没有角色、文件写不进去------很多人以为这命令只有多机集群才需要,单机也要。
坑二:桶名有规矩。 建一个叫 kb 的存储桶被拒------桶名至少 3 个字符(S3 协议的通用规矩)。最后改名 deepflux-kb。
坑三:病毒扫描默认"宁可拒收"。 没配扫描器时,文档上传后的入库动作直接被拒(行话叫 fail-closed,"拿不准就关门")。内网试点想放宽可以改 optional,但注释要求"必须向客户披露"。安全的默认值和"开箱即用"打架时,选安全,并把放宽的条件写成显式披露。
真出问题还有 diagnose.sh:一键导出脱敏诊断包(版本、日志、健康状态,敏感配置打码)给技术支持。
八、备份的设计:怎么做才敢睡觉
备份(backup.sh)有两个值得抄的设计:
知识库模式,不允许只备数据库。 数据库里存的是文档索引和向量,文档本体在对象存储------只备一半,恢复出来是空架子。所以备份包 = 数据库全量导出(pg_dump,PostgreSQL 自带备份工具)+ 对象存储快照 + 模型指纹 + 清单,缺一不可。
密钥不进备份,但密钥的版本号必须进。 平台有些数据(如登录配置)用主密钥加密后落库。备份带密钥明文,备份本身就是泄密源;但连"当时用的哪版密钥"都不记,将来恢复时新密钥解不开旧数据。所以强制要求一个密钥版本字段,缺了直接报错。
异地备份用 age 加密(极简文件加密工具,公钥上锁、配对私钥才能开;私钥放密钥保管系统,注释原话"不能把私钥或口令写入仓库/普通 .env")。恢复脚本(restore-offsite.sh)在解密后还要再过一遍路径和链接检查,且全包必须恰好一个数据库备份文件------恢复是最后一道操作,没有后悔药。
再念一遍边界:这套备份恢复有自动化门禁测试守着底线,但真实加密、真实异地上传、真实灾难恢复,都还没执行过。
九、真实发生过的事:自家云上的生产
对照一下仓库里真实发生过的部署面:自营云上的裸机栈(deploy.sh,536 行)。服务器是自己的,可以 SSH 直连;进程交给 supervisor(进程守护工具,崩了自动拉起);升级 = 本地编译、上传、热重启;前端文件"先传临时名、再一步改名"的原子切换。
生产运维手册(PROD_LIVE_RUNBOOK.md)记着真实事故。2026-06-21:线上聊天突然不可用,根因是 DeepSeek 的 API key 失效------一次真实的生产故障与修复。2026-07-19:线上站点的 /agents 页面 404,因为旧的冒烟检查脚本只验证了 chat 一个前端子应用,agents、tasks 等子应用漏传了没人知道。修复方式值得抄:新的 smoke.sh 不再硬编码"检查哪些",而是自己扫描目录、发现全部 14 个前端子应用,逐个验证入口文件可达。任何依赖"记得检查"的流程,迟早死于某一次没记得。
同一个仓库里,一面是带着日期的真实事故记录,一面是标着"未实测"的交付目标------两边都写得很清楚,这才是这套工程最值得学的地方。
小结:三条通则
- 版本化是信任的锚。 禁 latest、指纹清单、版本号只在体检通过后写入------离线交付里,一切"应该没问题"都要换成"可以核对"。
- 任何改动,要么做成,要么当作没发生。 临时文件+改名、先下载后切换、失败自动恢复------bash 没有数据库那种事务,但这三样能拼出近似物。
- "建成"和"验证过"是两个词,中间隔着三层账。 真跑过的、本机演练过的、从未执行过的------这套工程把三层分得清清楚楚,连测试脚本都自我声明"不宣称真实加密"。把没验证的说清楚,和验证本身一样重要。