速读#
我以前把部署理解成一串顺序固定的步骤:买 VPS、装 Docker、配 Nginx、申请证书、开防火墙、设置自启。服务增加以后,这张清单越来越长,却仍然会漏掉数据恢复、告警和回滚。后来我把它改成八个分面:可达、守护、防护、可观测、配置密钥、变更、数据和运维入口。每加一个服务,不是照抄所有工具,而是逐面确认需求、当前方案、验证证据和仍然接受的缺口。
这八面不是行业标准,也不是互相独立的八个盒子。它们只是可以分别提问的关注点,实际会相互影响:开放端口同时改变可达和防护,自动部署会牵动变更、数据和回滚,监控服务本身也需要守护。分面的价值不是把每个动作强行归类,而是避免“页面能打开”掩盖其他尚未处理的问题。
这个框架怎样形成#
转折来自一次 Cloudflare Tunnel 迁移。那次我只改变了外部请求怎样到达服务,systemd、数据目录、SSH、密钥和发布流程基本没有变化,网站仍然可以运行。这说明部署不必永远按一条固定流水线理解:入口方案可以替换,进程守护和数据策略仍然保留;反过来,Tunnel 让服务可达,也不会顺便解决进程退出、密钥泄露和备份恢复。
我最初把这些面称为“互相独立”,后来发现不够准确。更合适的说法是“可分开检查、需要联合验证”。例如把应用从公网端口迁入 Tunnel 后,应重新检查源站监听和防火墙;给容器加自动重启后,还要确认错误配置不会形成无限重启循环;自动部署数据库应用前,必须先知道升级失败时怎样恢复数据。单面配置成功,不代表组合起来仍然安全。
八个分面分别问什么#
| 分面 | 要回答的问题 | 最小验证证据 | 常见的“假完成” |
|---|---|---|---|
| 可达 | 合法请求怎样到达服务 | 从真实外部网络访问域名、端口或 Tunnel 路由 | 控制台显示在线,但外部实际不可达 |
| 守护 | 进程退出或主机重启后怎样恢复 | 主动停止进程并重启主机,确认服务按预期恢复 | 只写了 Restart=always,从未测试失败循环 |
| 防护 | 谁可以访问哪些入口 | 检查监听、端口、认证和未授权请求结果 | 只看 UFW,忽略 Docker 发布端口或应用权限 |
| 可观测 | 出事后怎样发现、定位和关联一次请求 | 健康检查、结构化日志、资源指标与一条真实告警 | 有 journalctl 就当成会自动通知 |
| 配置密钥 | 配置从哪里来,秘密怎样存放和轮换 | 仓库与构建产物无秘密,文件权限受限,轮换流程跑过 | .env 加进 .gitignore 就认为绝对安全 |
| 变更 | 新版本怎样发布、验证和回退 | 能指出当前版本,部署后有健康检查,旧版可恢复 | GitHub Actions 绿了就默认线上正确 |
| 数据 | 状态存在哪里,损坏或误删后怎样恢复 | 备份文件之外,至少完成一次恢复验证 | 挂了命名卷就把它当备份 |
| 运维入口 | 业务入口失效时从哪里管理和救援 | SSH、私网或云厂商控制台中至少有一条独立恢复路径 | 管理入口与故障服务共用同一条链路 |
表中写的是证据,不是指定工具。单台 VPS 可以用 systemd、SSH 和简单备份完成这些问题,规模更大时也可能换成编排平台、集中密钥系统和多区域恢复;工具会变,问题仍然存在。反过来,如果某一面在当前规模确实不需要,也应明确标成“不适用”并写出原因,而不是因为没有配置就假装没看见。
为什么不直接抄一份最佳实践清单#
清单适合防止重复遗漏,但别人的完整清单往往把规模、威胁模型和预算一起带了进来。我的博客运行在单台 VPS 上,没有必要为了“高可用”立即增加 Kubernetes、多区域数据库和一套企业密钥平台;这些组件本身也需要升级、监控和恢复,理解不足时只会增加新的故障面。
分面法让我先写风险,再决定最低成本的应对。没有多节点容灾,可以接受主机故障时需要人工恢复,但必须有异地备份和恢复步骤;没有完整 APM,可以先保留带请求标识的应用日志和服务级健康检查,但不能拿整机 CPU 正常证明接口可用;没有专用 Secrets Manager,可以使用权限为 0600 的环境文件和 CI Secrets,但仍要防止它们进入日志、构建产物和备份共享目录。
我现在给每一面只保留三种状态:已经验证、明确接受风险、不适用。过去使用的“评估中”太容易无限延期;如果确实要以后处理,就再写触发条件和复查时间,例如“出现第二位维护者后拆分账号权限”或“数据超过当前恢复窗口后改用增量备份”。这样缺口是明账,不是模糊的待办。
用一个自托管工具走一遍#
假设我要在现有 VPS 上增加一个带管理后台的小工具,最低可行方案可以这样写。可达性使用现有 Tunnel,把 cloudflared 与应用放进可通信的容器网络,不发布新的宿主机公网端口;这里的关键是“不配置 ports 并验证外部无法直连”,因为 Compose 的 expose 主要是声明信息,本身不是安全边界。
守护交给 Compose 的 restart: unless-stopped 或 systemd 中的一层,不让两个监督器互相拉扯,并通过杀进程和重启主机验证。管理路径放进 Access 或应用服务端认证,默认密码首次启动后立即轮换;整机监控继续复用,但新增一个能反映应用依赖的 /healthz,再确认故障时真的能收到告警,而不只是事后可以登录查日志。
配置文件可以进入仓库,秘密单独放在权限受限的环境文件或部署系统中,并检查 Git 历史没有旧值。变更以固定镜像版本或 digest 为入口,部署后跑健康检查,升级前记录旧版本和回滚命令。应用数据放入明确的卷或宿主机目录,再备份到另一处存储并抽样恢复;命名卷只解决容器重建后的持久化,不能抵抗主机损坏和误删除。日常管理走 SSH 或私网,仍保留云厂商控制台作为 Tunnel、网络或 SSH 同时失效时的最后入口。
这套方案不复杂,但每一项都能回答“怎样证明”。十分钟写完八面,比安装完成后才发现数据库留在容器可写层、管理端口暴露公网,或者备份从未成功恢复更省时间。
我最终保留的用法#
八个分面不是要求所有服务都做到同一等级,而是要求每个决定可见。上线前我会留下一张很短的记录:每面采用什么方案、验证时间、已知缺口、故障时先看哪里。服务变化后只更新受影响的面,同时检查相邻依赖,不必把整份部署文档从头重写。
我对“部署完成”的定义也因此变了:不是命令执行结束,也不是页面第一次打开,而是合法请求能到达、非法请求被挡住、进程和数据可以恢复、故障能够被发现、变更能够回退,并且我仍有一条独立入口去处理意外。八面只是帮助我把这句话逐项落实。
