跳到正文

Blog

部署到底归谁管:小团队如何划分 Git、CI、服务器和发布状态

从 Git 仓库、CI Runner、自有服务器到部署记录,拆清小团队部署链路里的四种所有权,以及 Appaloft 负责和不负责的边界。

AppaloftDeployment controlDocker ComposeSelf-hostingCI/CD

小团队把应用放上服务器时,最先讨论的通常是工具:Railway、Render、Dokku、Portainer、Docker Compose,或者一段 SSH 脚本。

工具当然重要,但它们常常掩盖了另一个问题:代码归谁管,CI 在哪里执行,服务器属于谁,部署完成后又由谁保存状态?这四件事可以由同一个平台负责,也可以拆开。边界没说清楚时,工具越多,出问题后越难回答“现在到底运行着什么”。

Appaloft 选择的是一条相对明确的边界:代码继续留在 Git,构建可以留在现有 CI,应用部署到你控制的服务器;Appaloft 负责把 source、目标服务器、部署记录、健康检查、访问状态和应用版本恢复连在一起。它不是 Git 托管服务,也不是 CI Runner 管理器,更不是通用服务器面板。

先分清四种所有权

一个常见的小团队部署链路里,至少有四个不同对象。

代码所有权属于 Git 仓库。仓库保存源码、Dockerfile、Compose 清单和非敏感部署契约。无论使用 GitHub、GitLab、Gitea 还是别的 Git 服务,仓库都不应该偷偷承担生产环境状态数据库的职责。

执行所有权属于 CI Runner 或发起部署的人。Runner 可以 checkout、测试、构建并调用部署入口。它需要短期凭据,但不应该因为能执行命令,就自动成为服务器、部署记录和恢复策略的长期所有者。

基础设施所有权属于服务器或云账号的持有者。VPS、SSH credential、DNS provider 和 registry 都有各自的权限及账单边界。客户拥有服务器,不等于客户应该接手一份只有原开发者看得懂的脚本。

部署状态所有权属于控制面。它要记录这次发布来自哪个 source、用了哪个 runtime plan、目标是哪台服务器、健康检查是否通过、当前访问入口指向哪里,以及是否存在可用的应用版本回滚目标。

这四类所有权可以放在一个托管 PaaS 里。选择自有服务器时,它们通常会被拆开。真正需要设计的是拆开以后怎样重新接起来。

CI 应该触发部署,不应该成为部署数据库

GitHub Actions、Gitea Actions 或自建 Runner 都很适合执行工作:拉代码、跑测试、构建 image、调用 CLI。问题出在团队把所有部署事实都留在 CI log 里。

几周后再看,一个绿色 job 只能证明当时的脚本以零状态码结束。它不能单独证明服务器拉取了目标 artifact、代理已经切换、health endpoint 通过,或者线上运行的仍是那次部署创建的 workload。

我们更倾向于让 CI 做入口,让部署控制面保存长期状态。官方 GitHub Action 是 Appaloft 当前提供的一条集成路径;其他 CI 也可以调用同一套 CLI 或 API,但 Appaloft 不负责创建、隔离或维护这些 Runner。

这个边界也解释了为什么“支持 Git source”不等于“原生管理每一种 Git 服务”。仓库提供 source,Runner 提供执行环境,部署控制面不应该假装拥有前两者。

Compose 是部署输入,不是完整的服务器管理

Docker Compose 很适合小型应用。它把 service、network、volume 和 environment 的关系放在一个可 review 的文件里,也比一串留在 shell history 里的 docker run 更容易恢复。

Appaloft 可以把 Compose 清单作为 deployment source,并将一个多 service stack 建模为一个可部署 Resource。例如:

name: acme-web
source:
  path: .
runtime:
  strategy: docker-compose
  dockerComposeFilePath: docker-compose.yml
network:
  internalPort: 8080
health:
  path: /health

这份配置说明 source、运行方式、公开入口和健康检查,但它没有把整个服务器交给 Appaloft。操作系统更新、磁盘空间、Docker daemon、备份介质和主机安全仍属于服务器运维。

同样,Appaloft 也不是 Portainer 的等价替代。它不会把任意现有容器都接管成受控部署,也不承诺镜像漏洞扫描、主机文件浏览或所有容器的通用管理。它关心的是由 Appaloft 创建和记录的部署,以及这些部署怎样被验证和恢复。

“部署成功”需要比进程存在更强的证据

一个进程在运行,不代表新版本已经接管流量。一个 endpoint 返回 200,也可能只是旧容器仍然健康。

至少要分别检查:

  • runtime 是否真的启动了本次部署的 workload;
  • health profile 指定的路径是否返回预期状态;
  • route 是否仍指向当前 Resource;
  • deployment record、runtime identity 和访问结果能否对上;
  • 上一个已验证部署是否仍可作为 recovery candidate。

Appaloft 的 health profile 会把路径、状态码、timeout、retry 和 start period 留在 Resource 上,供后续部署复用。部署历史不会因为 profile 修改而被重写。日志和健康状态也应该回到同一个 Resource,而不是散落在 CI、SSH terminal 和代理面板里。

这部分的目标不是制造一个更大的 Dashboard,而是减少互相矛盾的答案。CI 说成功、容器说健康、域名却打不开时,控制面必须把这些证据分开呈现,不能选一个绿色信号替大家下结论。

应用回滚不等于恢复全部状态

Appaloft 的 rollback 会基于保留的成功 deployment snapshot 和 Docker/OCI artifact 创建一次新的应用部署尝试。它不会重新按当前 Resource profile 猜测旧版本,也不会修改原来的失败记录。

这个能力有一个必须说清楚的限制:它不自动回滚数据库、Volume 或外部依赖。

如果新版本已经执行了不可逆数据库迁移,换回旧 image 可能让情况更糟。Compose 里的 named volume 仍然是运行状态,不会因为应用版本回退就自动恢复到昨天。对象存储、消息队列和第三方 API 也有自己的恢复流程。

所以真正可用的恢复计划至少分成两层:

  1. 应用层:保留 verified deployment、artifact identity 和配置 snapshot,用于判断能否回滚应用版本。
  2. 数据层:单独准备备份、restore 演练、迁移兼容策略和人工确认边界。

如果第二层没有准备好,产品应该把它显示成 manual recovery gap,而不是把一个“Rollback”按钮包装成完整灾难恢复。

什么时候选托管 PaaS,什么时候保留服务器所有权

如果目标只是尽快把第一个 Spring Boot、Node 或静态站点放到线上,托管 PaaS 往往更合适。数据库、TLS、日志和发布入口集中在一起,团队不用先学会维护一台服务器。

自有服务器路径适合另一组约束:已经有 VPS 或内网机器,希望保留 runtime 和配置控制,需要在 CLI、CI、Web 或 agent 之间共享部署状态,或者不想让生产部署只剩一段只有作者理解的 SSH 脚本。

这不是“简单”和“专业”的区别,而是谁承担维护成本。托管平台把一部分责任收进服务里;自有服务器把责任留给团队,再由控制面减少重复操作和状态分裂。

Appaloft 服务的是后一条路径。你可以从部署来源开始,确认当前交付物是本地目录、Git 仓库、Docker image 还是 Compose 清单;再配置健康和网络 profile,让 verify 阶段检查真实访问路径。发生失败时,先读取部署恢复状态,再决定 retry、redeploy 或应用版本 rollback。

部署链路不一定需要由一个产品全部包办。但每个对象都应该有明确的所有者,每次交接都应该留下可读的状态。代码在哪里、谁执行、服务器属于谁、部署事实由谁保存——这四个问题答清楚以后,工具选择反而会简单很多。