KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA
私有化交付工程:从「在我机器上是好的」到客户能自己收场 — keel 龙骨
私有化交付工程:把一套系统送进客户内网 的参考信息:私有化交付工程:从「在我机器上是好的」到客户能自己收场
7 章 · 每一章都建立在一套可运行的交付包与一条真实的升级链上
交付最难的一刻,不是把系统做出来的时候,而是它在一个你进不去的地方不工作时。
「明明在我机器上是好的」——这句话在私有化交付里不是一句抱怨,它是一个结构性事实:
你依赖的那些东西(镜像能从仓库拉、依赖能在线装、日志有人自动收、出事能立刻登上去看)
在客户机房里一样都不在。所有的设计问题都是从这句话倒推出来的。
这门课从一条真实的交付链出发:把一套最小系统打成一个自带全部输入的包,
搬进一个没有出口的机房,按固定顺序落地,然后升级、回滚、排障。
每一条约束都配一次故意破坏——把停机窗口改小、把迁移写坏、把数据库停掉、把镜像漏掉——
看它从哪里断、断的时候现场留下了什么。
三个现场
现场一:包搬过去了,起不来,报的却是「网络问题」。
镜像没从包里导进来,docker compose up 的默认行为会去仓库取。客户内网没有出口,
于是屏幕上先出现 Pulling,然后是几十秒的等待,最后落到一句 failed to resolve reference。
看起来像网络不稳,真相是制品根本没进来——而这段等待把最贵的东西耗掉了:现场那几分钟。
现场二:升级「成功」了,但任务永远停在处理中。
worker 手上一个任务要跑二十秒,而停机窗口被设成了五秒。docker stop 等到窗口用完就 SIGKILL,
任务被砍在中间——不报错、不留痕,只留下一行永远停在 running 的数据,
而且重启 worker 也不会收拾它,因为新 worker 只领 pending。
现场三:回滚命令跑了,退出码 0,什么都没发生。
用旧镜像去退结构,而旧镜像的迁移表里根本没有新那一版——于是 --to N 变成一次静默的空操作。
命令成功、退出码 0、库还在新结构上,然后起栈时探针喊「库结构版本对不上」。
这一条的教训比它本身值钱:退出码为 0 不等于它做了你以为的那件事。
这门课回答什么
| 你可能正在纠结的问题 | 在哪一章 |
|---|---|
| 私有化和普通部署到底差在哪几段? | 01 |
| 一个能送进内网的镜像里装了什么?为什么代码很小、镜像很大? | 02 |
| 没有外网怎么把制品搬进去?「能拉」为什么不能假设? | 03 |
| 从压缩包到跑起来隔着几步?哪一步失败会留下半成品? | 04 |
| 一套代码怎么给多个客户在同一台机器上各起一套? | 05 |
| 停机窗口该设多少?为什么任务会永远停在处理中? | 06 |
| 升级怎么做到「要么升上去、要么原样退回来」?回滚为什么最容易写错? | 06 |
| 客户说「不对」的时候,他先能自己查出什么? | 07 |
| 这套形态明确不解决什么? | 07 |
这门课不回答什么
- 不重讲交付形态怎么选。 「这个场景该做成内网交付还是多客户共用一套」是
《构建打包与部署上线》的形态演进一章,
本课从「已经选定私有化」之后开始。 - 不重讲 Dockerfile 语法、层与缓存、compose 基础、信号与停机机制。
那些在《Docker 应用课》,
本课只讲它们面向交付的那部分后果。 - 不重讲 CI 与门禁。 本课从「制品已经打好了」开始,之前那一段在
《CI 流水线与质量门禁》。 - 不讲 K8s。 这一点是刻意的:私有化现场多数不装 K8s,
所以本课全程只用 compose 这一层的原语。已经在用 K8s 的读者,
探针、资源预算与滚动更新在《Kubernetes 应用课》——
但要注意那一层的名词搬到这里不一定有承接它的东西。 - 不做名词词典。 每个词从哪来、严格定义是什么,本课只在第一次用到时给一句够做判断的定义,
完整口径见course/glossary.md。
章节地图
01 为什么私有化交付不一样 ───────────── 先站到高处:这条链路和普通部署差在哪
│
▼
02 一个自足的镜像 ──────────────── 要搬的到底是什么,为什么这么重
│
▼
03 没有外网怎么把制品搬进去 ─────── 跨过一段没有网络的路
│
▼
04 从压缩包到跑起来 ────────────── 现场五步,顺序错了会留下什么
│
▼
05 配置注入与多实例 ────────────── 同一份镜像怎么给多个客户各起一套
│
▼
06 升级与回滚 ─────────────────── 怎么让结局只有两种
│
▼
07 现场排障与边界 ─────────────── 把能自己查的交出去,并说清不做什么
七章的分工是**「先拆链路、再看制品、再搬、再落地、再并存、再演进、最后收口」**:
01 划清与普通部署的界线,02–03 把「要搬的东西」和「怎么搬」讲透,
04–05 讲现场的落地与多客户并存,06 集中处理可逆性,07 收在排障与边界上。
配套项目
course/project/ 是一套只用标准库的最小系统加一整套交付脚本:
app/board/ 七个源文件:config / log / db / web / worker / __main__
Dockerfile 多阶段、非 root、刻意不带 HEALTHCHECK
deploy/self-hosted/ compose.yml + env.example + install / upgrade / rollback / selfcheck
deploy/build-bundle.sh 打离线包(跑在有外网的那一侧)
deploy/variants/ 初版 1.0.0 的差异文件(让 06 章的升级演练有真实的起点)
它不替代任何真实产品,只做一件事:把「交付链路上到底发生了什么」变成可读、可改、可破坏的代码与脚本。
五个角色(web / worker / migrate / config / doctor)共用同一个镜像,
靠启动命令区分——这一个决定就带出了本课三分之一的判断。
学完以后你能做什么
- 拿到一个「要送进内网」的需求,能说出这条链路上哪几段与普通部署不同、为什么;
- 读出镜像的构成与那三处「故意不写」的设计,并说出每一处各自在防什么;
- 说清「现场拉镜像」为什么走不通,以及正确的搬运动作加它的三条命名约定;
- 列出一次安装的步骤,并对每一步说出「它失败时现场已经发生了什么」;
- 给出「一套代码装多个客户」的最小隔离集,并指出哪几个名字会撞车;
- 设计一次只有两种结局的升级,并说出回滚时执行迁移的镜像该取哪一版;
- 客户报障时按「哪一层断了」的顺序定位,而不是逐个配置项试;
- 说清这套形态不解决什么,以及那些部分该交给谁。
关于证据口径
- 正文里的编排解析读数、容器状态读数与日志行,来自 Docker Engine 29.8.2 + Docker Compose v2
这套环境;容器语义是 Linux 的(SIGTERM、stop_grace_period、internal网络),
与宿主操作系统的进程语义不同。 - 被交付物是
board的两个版本1.0.0与1.1.0,另有一个只用于演练「升级失败自动回滚」的坏版本1.2.0。1.0.0里那个「被SIGKILL的任务永远停在running」的缺陷是刻意保留的——
它是 06 章那组正反对照的前提。 - 基础镜像
python:3.12-slim、数据库镜像postgres:16-alpine、依赖psycopg[binary]==3.2.9,这几项固定。 - 读形状,别背数字。 磁盘可用量、命令耗时、容器 id、现场随机生成的数据库密码、归一化后的字节串,
全都随环境变;正文引用它们是为了让你看到值与关系。 - 本课的证据只来自这套
board交付链。 其它课程有各自的实验环境与版本,
不要把版本号互相当依据引用。协议与工具的行为以官方文档为准(见course/research-and-course-basis.md的出处清单),
课程里的判断与建议是本课自己的口径,两者分开标注。
课程导读(起点自检、章节地图、主案例、前置要求)见 course/README.md。