Jack 的 AI Blog

把博客搬到自己的服务器后,我用 WorkBuddy 接管了运维

从 Vercel 迁到一台自己的云服务器之后,真正的麻烦才刚开始。这篇文章记录我用 WorkBuddy 处理静默宕机、扫描器骚扰、性能调优和自动发布的完整过程,以及几件我认为必须特别小心的事。

上一篇文章里,这个博客还是靠 Vercel 一键部署的——推代码,等两分钟,站点就更新了。省事到几乎没有存在感。

后来我决定把它搬到一台自己的云服务器上。理由很朴素:手上正好有一台闲置的轻量服务器,而且我确实想亲手摸一遍从系统到 Web 服务器这一整条链路,而不是永远只有一个「Deploy」按钮。

搬家本身不难。难的是搬完之后发生的事。

第一个坑:站点已经挂了将近一个月

搬过去之后我挺自信——服务器管理面板上清清楚楚写着「运行中」。

然后 WorkBuddy 做了件很直接的事:先用 ss -tlnp 看端口在不在听,再用本机 curl 带上 Host 头去请求真实页面。

结果是 502。

顺着这个结果往下查,发现是三个问题叠在一起:

  • 面板里配置的启动命令指向的是开发模式,而不是生产模式
  • 而我的项目其实是静态导出配置,next build 产出的就是纯静态文件,根本不需要一个常驻的 Node 进程
  • 服务器只有 1.8 GB 内存,开发模式跑不动,被系统 OOM 掉了好几次,进程没了也没有任何东西把它拉起来

三个问题互相掩盖,所以从外面看只是一个 502。

这里最值得记住的一点是:面板上的状态和站点的真实可用性,是两件完全不同的事。 面板看的是「进程是否存活」,而我在意的是「用户能不能打开页面」。这两者之间可以差出整整一个月。

修复方式最后反而很朴素:既然构建产物是纯静态的,那就让 Web 服务器直接托管静态目录,把常驻进程整个删掉。少一个进程,就少一个会挂掉的东西。

第二个坑:不知道是谁在敲我的门

站点恢复之后,我去翻了访问日志。

三十多天、七万五千多次请求里,有相当一部分来自我完全不认识的 IP,在成批地试探 /wp-admin、/.env、/phpmyadmin 这类路径——典型的批量扫描器,跟你有没有用 WordPress 毫无关系,它们只是把网段扫一遍。

处理思路是在 Web 服务器层做降噪:

  • 空 User-Agent 的直接拒绝
  • 已知扫描器特征、常见漏洞路径、脚本后缀,一律拒绝
  • 单 IP 请求频率限流,超了返回 429
  • 把拦截日志单独切出来,别让它把正常日志淹掉

最后拦掉了大约一半的恶意请求,日志体积从 30 MB 降到 7 MB 出头。

这里有个反直觉的点。 我一开始以为「拦得越多越好」,实际上更重要的是可观测。把拦截日志单独拆出来之后,我才能看出哪条规则真在起作用、哪条只是在误伤。一个你看不见的拦截规则,本质上跟没有规则差不多。

第三件事:先测出真实上限,再谈优化

我的服务器规格很紧:2 核、1.8 GB 内存、40 GB 磁盘,还有一个容易被忽略的硬约束——出网带宽只有 3 Mbps。

在动手调优之前,WorkBuddy 先做了件我觉得非常对的事:先压测,测出真实瓶颈,再决定往哪儿使劲。

测下来,Web 服务器在保持连接的情况下每秒能处理一千多次请求,理论上限接近两千。但真正卡脖子的其实不是 CPU,而是那条带宽。

方向一下就明确了:

  • 开启 BBR 拥塞控制
  • 限制系统日志占用(从六百多 MB 压到 200 MB 上限)
  • 静态资源全部启用 gzip 预压缩,缓存策略拉满

结论是:CPU 和内存都不是瓶颈,带宽才是。 这个判断直接决定了后面所有的取舍——既然卡在传输上,那就把体积压到最小,而不是去优化计算。

第四件事:把发布变成一条命令

前面几件事本质上都是「一次性」的。真正每天要面对的,是怎么发文章。

最早的做法很常规:本地改完推送到 GitHub,服务器上定时拉取、构建、上线。

用了一阵子发现一个问题:GitHub 变成了试验田。 如果我本地写错了代码,推上去之后服务器构建失败——那个坏版本已经躺在远程仓库里了,得手动去收拾。

于是把顺序整个倒过来:

改内容  →  本地提交(先不推)
        →  构建
        →  五项自检(首页 / RSS / 404 页 / 随机一篇文章 / 标签页)
        →  全部通过  →  才推送 GitHub 做备份
        →  任何一步失败  →  回滚上一版本 + 撤销提交,GitHub 完全无感

这套流程跑起来之后,我最满意的其实是失败路径。

我特意塞了一段故意写坏的代码去做测试,结果是:站点照常访问没有挂、GitHub 上依然停在上一版没被污染、那个坏掉的文件还老老实实留在工作区里方便我查——而那个提交已经被自动撤销了。

一条命令,坏结果永远出不了本地。

说几条我实际的感受

过程讲完了,说说好处。

它不迷信面板,只认实测。 如果只信管理面板那句「运行中」,我可能还得继续让站点挂着。它更愿意去查端口、去带 Host 头请求真实页面,把「进程活着」和「页面能打开」当成两个独立的事实验证。

它会先确认自己的能力边界,再动手。 比如发布流水线要跑通,得先确认三件事:服务器能不能连通 GitHub、手上这把密钥是什么类型、有没有写权限。这些都是先做了实测才继续往下走的,而不是先假设没问题。

它会给出可复现的证据,而不是结论。 这点我特别看重。运维里最怕的就是「我觉得它是好的」。它会把手上的命令、原始输出、期望值和实际值一起摆出来。

出问题时会回滚,而不是留一堆烂摊子。 构建失败就恢复上一版本、撤销提交、把失败的版本记下来避免反复重试同一份坏代码。

经验会沉淀。 一件事处理完,这套流程会被写成文档留下来。下次遇到同类问题,不用从零开始摸索。

有几件事我建议你特别注意

下面这些是我踩过的、或者差点踩的坑。

凭据怎么放,是第一个要解决的问题

「让 AI 帮你操作服务器」这件事,本质上是把登录凭据交给它。

我的做法是把凭据放在本地一个配置文件里,用 .gitignore 排除,绝不进版本库。但说实话——更稳妥的做法是用 SSH 密钥登录,并且给自动化用的那把密钥单独限定权限。密码登录确实省事,可它同时也意味着:任何拿到这个密码的人都能直接进服务器。这已经列进我的待办清单了。

仓库是公开还是私有,决定了你能备份什么

我一开始想让 WorkBuddy 顺手把服务器上的运维配置——防护规则、调优参数——也一起备份进仓库。

然后意识到一个问题:这个仓库是公开的。 把防护规则一条条写得清清楚楚公开出去,等于顺手附了一份怎么绕过它的说明书。

所以最终决定:只备份博客代码,运维配置留在服务器上。

如果你确实想让配置也进版本库,那就先把仓库改成私有的。

权限有多大,风险就有多大

要让自动化流程把代码推送到主分支,就意味着服务器上存着一把对这个仓库有写权限的密钥。而同一台服务器上还有部署脚本,能以 root 身份执行命令。

这两件事加在一起,等于「能往主分支推送 = 能在这台服务器上执行任意命令」。

这不是危言耸听,部署流程本身就必须这么设计——它要能拉代码、装依赖、构建、重启服务。所以要记住:这类密钥需要定期轮换,服务器本身的登录权限也要一并收紧。

别让它替你判断「成功」

AI 能跑命令、能读日志,但它一样会误判。

我就遇到过一次:它根据输出判断某段内容没上线,实际上是我俩用的那条文本过滤写法在不同系统上行为不一致,导致它自己的匹配结果为空——是工具误报,不是部署失败。 白折腾了一轮。

教训是:要求它给出可复核的证据。 具体命令、原始输出、期望值、实际值。凡是「结论」,都应该能被第三个人用同样的命令复现出来。

还有一类问题:看起来一切正常

这次搬家还暴露出另一种更难受的情况——系统看起来是好的,但某个角落其实是坏的。

比如有个提交明明早就写好了,却因为流程没接上,在仓库里躺了将近一个月才真正上线;比如构建完全成功、首页正常打开,但某些非英文的页面路径其实是 404——因为构建工具会把这类文件名写成转义形式,而 Web 服务器在处理请求时会先解码再去匹配文件,两边对不上。纯英文的路径完全看不出这个问题。

这类问题没有通用解法,只能靠逐条实测发现。

我现在的发文流程

  1. 有新文章,我把内容整理好交给 WorkBuddy
  2. 它写进内容目录,跑一条发布命令
  3. 服务器上构建、自检、上线
  4. 全部通过之后,才把代码推到 GitHub 备份

中途任何环节出问题都会自动回滚,最坏的结果不过是「这次没发出去」,站点不会挂,远程仓库也不会被污染。

最后

把博客搬回自己手里的服务器,技术上不算难。难的是意识到——有太多「看起来正常」其实并不正常的状态:进程活着但页面打不开,面板显示运行但站点早已下线,构建成功但某个页面是 404。

这类事靠人肉盯着很累,交给一个会实测、会回滚、会把经验记下来的工具,确实轻松很多。

但前提是:你得清楚哪些东西不能交给它。 比如权限的边界,比如哪些配置不该被公开。

工具能帮你把事做对,而「什么叫做对」,还是得你自己定。


← 返回文章列表