跳到正文

Blog

部署平台不该假设所有应用都跑在 3000 端口

从 Vite、Python、Go 到 Docker Compose,Appaloft examples 如何用明确的部署契约覆盖不同 runtime,而不是靠平台猜测。

AppaloftDeployment controlRuntimeDockerExamples

最小部署示例很容易写成这样:一个 Node server,监听 3000 端口,提供 /health,旁边放一个 Dockerfile。

它适合验证最短路径,却也容易让部署平台形成错误的世界观。真实仓库里还有构建后交给 nginx 的 Vite 站点、监听 8000 的 Python 服务、编译成单个 binary 的 Go 服务,以及同时包含公开 Web 和内部 API 的 Docker Compose stack。继续拿一个 Node hello world 代表所有应用,很多边界永远不会被碰到。

我们最近扩充了公开的 appaloft/examples 仓库。目的不是展示五种语言的入门写法,而是给部署控制面准备一组形状不同、可以在本地 smoke、也可以从 Git 仓库部署的测试对象。

不是猜语言,而是读契约

Appaloft 不需要看到 package.json 就猜这是 Node,也不该因为发现 main.go 就擅自选择构建方式。文件名只能提供线索,不能代表用户意图。

每个 Git 部署示例都带有 appaloft.yml。Vite 示例的契约很短:

name: appaloft-example-vite-static
source:
  path: .
runtime:
  method: docker-image
network:
  internalPort: 8080
access:
  default: public
health:
  path: /health

这里没有要求控制面理解 Vite。Dockerfile 负责 npm run build,再把产物交给 nginx;部署契约只说明怎样构建 runtime、流量应该进入哪个端口,以及用什么路径检查健康状态。

Python 示例把 internalPort 改成 8000,Go 示例使用 8080。这几个数字看起来不起眼,却能及时抓出一种常见错误:某段部署代码、反向代理配置或 smoke 脚本偷偷把 3000 当成了常量。

同一个目录还不够

语言不是唯一变量。monorepo 还要求部署系统准确处理 base directory。

公开 examples 仓库把每个应用放在独立目录中。部署 vite-static 时,source 仍是同一个 Git 仓库,但 base directory 是 /vite-static;部署 Go 服务时则是 /go-http。构建上下文、Dockerfile 和配置文件都应该相对这个边界解析,不能偶然从仓库根目录找到另一个应用的文件。

这也是我们保留 hello/ 作为最小官方 launch smoke,同时增加其他示例的原因。稳定的最短路径适合持续检查产品入口;更宽的 catalog 用来覆盖端口、构建过程、语言和拓扑差异。让每一种变化都成为默认 smoke,只会让失败难以定位。

Compose 改变的是拓扑

compose-stack 不是再加一种语言。它改变了部署对象的形状。

这个示例包含两个服务:公开的 web:8080 和内部的 api:3000。它的声明使用 runtime.strategy: docker-compose,并指向 docker-compose.yml。Web 服务通过 Compose 网络访问 API;外部流量只进入 Web。

runtime:
  strategy: docker-compose
  dockerComposeFilePath: docker-compose.yml
network:
  internalPort: 8080

如果部署系统只会问“容器监听哪个端口”,它很难表达这里的事实:有多个 workload,但只有一个公开入口;内部服务需要被创建和连通,却不应该各自获得公网 route。Compose 示例迫使控制面区分 runtime topology 和 access topology,而不是把两者压成一个端口字段。

环境变量需要一个不会泄密的例子

env-service 专门演示非密钥配置。它声明 APP_NAMEGREETINGFEATURE_FLAG,再通过 /api/config 返回可公开观察的结果。

我们刻意没有放数据库密码或 token。示例仓库应该让人看懂变量如何从部署输入进入 workload,但不该训练用户把 secret 提交进 Git。真正的 secret 需要走部署平台的受保护输入;readback 和日志也不应返回原值。

这类例子比 README 里写一句“支持环境变量”更有用。它可以被 smoke 脚本实际调用,证明容器拿到的是部署时的值,而不是 Dockerfile 里的默认值。

一个 smoke 命令,覆盖不同失败边界

仓库根目录提供 ./scripts/smoke-all-local.sh,各个示例也有自己的 scripts/smoke-local.sh。它们覆盖的并不只是“进程能启动”:

  • Vite 需要先构建静态产物,再由 nginx 提供 /health
  • Python 和 Go 会暴露错误的端口假设;
  • env-service 需要验证配置 readback;
  • Compose 需要同时验证多服务启动、内部连通和公开入口。

本地 smoke 不能替代部署到真实控制面的验收。它不会证明 Git source、远程 server、route 或最终 URL 正确。但它能更早回答一个便宜得多的问题:这个示例本身是否成立?如果本地容器都无法通过健康检查,就没有必要先把失败带进远程部署链路。

示例也是产品接口

部署示例很容易被当成文档附件,写完以后慢慢过期。我们更愿意把它们当成接口测试的一部分。

一个示例同时连接了用户会复制的仓库结构、appaloft.yml 契约、Docker 构建、runtime 网络和 smoke 验证。修改部署语义时,如果这些例子不再成立,问题不只是 demo 坏了;它可能意味着文档、控制面和用户仓库之间已经出现了分歧。

我们仍然没有覆盖所有形状。需要外部数据库的应用、带持久卷的服务、worker、定时任务和更复杂的多服务依赖,都应该在有明确验收目标时加入,而不是为了让 catalog 看起来丰富就堆进去。

这组 examples 先解决一个更基础的问题:不要让部署平台只在最熟悉的应用上显得正确。应用可以用不同语言、端口、构建方式和拓扑;平台需要的是一份明确契约,以及能证明它确实被执行的证据。后半部分,我们在只有运行中的工作负载真的变了,部署才算成功里继续讨论。