换服务器手册:一个包搬走整站

站点静态化之后,换服务器从「折腾两天」变成「拷一个文件、跑一个脚本」。

但前几版方案有个隐患一直没解决:备份包不含历史图片。1335 张 WordPress 时代上传的图活在 wp-content/uploads/ 里,不在 Hugo 源码里,构建产物里也没有。真换机器时,只搬源码等于搬了个没有配图的博客。

这次索性把话说透:本地电脑一个字节都不留,所有能丢的东西都放服务器上,然后用一个包把它们全串起来。

先说清哪些东西会丢

搬家方法论没意义,先列清单。一个博客的资产分三类,丢失后果完全不同:

资产 在哪 丢了会怎样
169 篇源码 site-real/ 全站内容
WordPress 原始导出 archive/wordpress-real.wxr.xml 再也无法回溯原始数据,只此一份
350 条 301 映射 archive/reports/redirect-map.csv 老链接全变 404,搜索引擎收录作废
1335 张历史图片 线上 wp-content/uploads/ 所有文章配图变破图
评论 /var/lib/isso/comments.db 评论全丢,含待审核队列
nginx 配置 面板的 vhost 目录 301、评论反代、公众号回调全没
TLS 证书与私钥 面板的 cert 目录 新机器 nginx 起不来(vhost 里有 listen 443)
isso.cfg /opt/isso/ 评论服务起不来
公众号回调代码 站点内 mp-weixin/ 发消息没反应,且它不在 Hugo 产物里
hugo 二进制 .bin/hugo 得重下,墙内下 GitHub 常断在几十兆
SSH / 面板口令 你的密码管理器 进不去机器,其余全部作废

前四行里,只有第一、二、三行在源码目录里。图片、评论、配置、证书、公众号后端、二进制全散落在系统路径下——这就是为什么「打包源码」从来不算备份。

一个包,把上面全装进去

cd /home/ubuntu/cc/blog
./blog snapshot

产出 snaps/blog-full-<时间戳>.tar.gz,112 MB:

site-real/ tools/ deploy/ out-real/ archive/ ops/ backups/   ← 源码、原件、配置、退役备份
public/                                                      ← 已构建好的产物,699 个文件
uploads/                                                     ← 1335 张历史图片
var/isso/comments.db                                         ← 评论库
ops/cert/                                                    ← TLS 证书与私钥(600)
ops/mp-weixin/                                               ← 公众号回调(含 AESKey,600)
ops/vhost/ ops/isso/                                         ← nginx 与评论配置
backups/retired-20261113/                                    ← 退役旧站的唯一副本
bin/hugo                                                     ← 62 MB 的二进制,一并带走
SNAPSHOT.json  SHA256SUMS  restore.sh                        ← 清单、校验、还原脚本
README.md  MIGRATION.md                                     ← 说明书

几个刻意的选择:

产物也打包。 新机器铺完就能开站,不依赖 hugo 在那台机器上构建成功。多 15 MB 换一个「哪怕构建环境坏了站也是活的」的兜底。

hugo 也打包。 62 MB 的二进制让包从 15 MB 涨到 112 MB 的大头之一,听着浪费。但换机器那天最不想遇到的就是「下 hugo 卡在 43% 报 SSL unexpected eof」。包里带着,解压就有。

WXR 原件也打包。 之前的方案把它排除在外,理由是「含历史访客信息,不能乱传」。但那是在公开分发的场景下。现在这个包只在自己机器之间挪,它就是唯一原件,不带上等于没备份。

退役旧站备份也打包。 这条是后来补的,补之前差点出问题:旧 WordPress 目录删掉之后,它的唯一副本就在 backups/ 里,而快照当时用的是显式目录清单,没带上它——等于换机即丢失。现在 blog snapshot 和 restore.sh 都会处理它。

凭证一个都不打包。 SSH 密码、面板密码、admin-password.txt 都在包外。包会被拷到 U 盘、传到网盘,不该带着钥匙。

注意一个例外:证书私钥和 wx-config.php(含公众号 AESKey)是在包里的。它们必须跟着走,否则新机器起不来。但这意味着这个包不能往公开位置放——要传网盘先 gpg -c 加密,理由和上面那条正好相反:不是「不该带钥匙」,而是「带了钥匙,所以包本身就是钥匙」。

五步搬过去

1. 打快照,拉下来

# 老机器
./blog snapshot
scp ubuntu@旧IP:/home/ubuntu/cc/blog/snaps/blog-full-*.tar.gz .

2. 新机器备环境

必须的只有 nginx、python3、sqlite3,加一个 www 用户。其余全在包里。

3. 解包,先干跑

mkdir -p /tmp/new && tar -xzf blog-full-*.tar.gz -C /tmp/new
cd /tmp/new && sudo ./restore.sh --dry-run

干跑会把八步操作全部打印出来但不写盘,先看清楚它要动哪些目录,比自己脑补靠得住。

4. 真跑

sudo ./restore.sh

默认路径按宝塔布局,别的位置用参数覆盖:

sudo ./restore.sh --root /home/ubuntu/cc/blog \
                  --webroot /www/wwwroot/blog-static/public \
                  --confdir /www/server/panel/vhost/nginx

八步(第 5 步现在还会铺评论配置并检查服务):

0/8 sha256 校验包完整性
1/8 检查环境(没有 python3 直接退出)
2/8 铺源码与退役备份,属主交还给实际使用者
3/8 铺现成产物
4/8 还原 1335 张历史图片
5/8 还原评论库(有旧库先备份成 .bak-时间戳)
    并铺 /opt/isso/isso.cfg 与 isso.service,然后启服务
6/8 站点文件权限 www:www 755/644
7/8 写 nginx vhost(含证书),nginx -t,reload
8/8 自检:首页文件、文章页数、图片张数、HTTP 状态码

第 5 步有个必须知道的例外:评论程序本身不在包里。/opt/isso/venv 有 27 MB 且与 Python 版本绑定,跨机器不可靠,所以由脚本重建。如果新机器上还没有 venv,restore 会明确提示:

! 没找到 /opt/isso/venv —— 评论程序需先安装:
  sudo /home/ubuntu/cc/blog/deploy/install-isso.sh   # 建 venv 并装 isso==0.14.0(1-3 分钟)
  然后:sudo systemctl restart isso
  (页面与历史评论不受影响,只影响发表新评论)

这一步为什么不静默跳过:评论坏了不影响页面打开,首页、文章、图片全是 200,很容易几个月后才发现评论区不能用。宁可提示吵一点。

5. 切解析

DNS 指到新 IP。老机器别急着关,两台并行跑三天,对比没问题再动手。

演练时踩到的六个坑

上线前在隔离目录里完整演练过一遍,跑出这些问题,都是真金白银:

一、--prefix 改了,数据目录没跟着走。 早期脚本不管装到哪儿,数据库路径都写死 /var/lib/isso,演练时差点连上生产库。现在规则很明确:装到 /opt/isso 用系统目录,装到其它前缀时数据和日志都落在 prefix 内部。

二、少了 X-Script-Name,后台全挂。 Isso 靠这个头生成带前缀的绝对 URL。缺了它,/isso/admin 会跳到 /admin/,CSS/JS 全部 404。nginx 反代里必须写:

proxy_set_header X-Script-Name /isso;

三、admin 会话 Cookie 跟着每个静态资源请求跑。 加一行 proxy_cookie_path / /isso/; 把 Cookie 限制在 /isso/ 作用域内。

四、自检 200 但评论加载不出来。 --confdir 指到了一个没被 nginx 加载的目录,配置写进去等于没写。

五、sudo 铺出来的文件,普通用户改不了。 这是最后一次演练才发现的:脚本用 sudo cp -a 铺源码,文件属主全是 root。站点能打开、nginx 正常,看起来一切正常,但第二天用普通账号 blog build 直接 permission denied——因为 Hugo 要往 prod-public/ 写。修法是铺完立刻 chown -R 给实际使用者(SUDO_USER),而不是留给下一个人去猜。

这个坑的教训比修法重要:迁移脚本的自检不能只看「站点能不能打开」。能打开只证明 nginx 找对了目录,不证明这台机器明天还能用来写文章。

六、没有证书时,光摘掉 443 会让全站打不开。 这个最凶。HTTPS 上线后 vhost 里多了 listen 443 ssl 和一条强制跳转。如果新机器没带上证书,nginx 会因为读不到证书文件直接拒绝启动;更麻烦的是,即使你把 listen 443 那行摘掉,强制跳转还在——所有请求会被 301 到一个根本不存在的 https 端口,站点表现为「完全打不开」,而 nginx -t 是通的,很容易查错方向。

所以两件事必须绑在一起摘。restore.sh 里的实际写法:

if [ "$TLS_KEEP" != 1 ]; then
  SED_EXPR="$SED_EXPR;/listen 443 ssl/d;/http2 on;/d;/ssl_/d"     # 去掉 443
  SED_EXPR="$SED_EXPR;/if (\$blog_force_https)/,/^    }/d"        # 去掉跳转
  SED_EXPR="$SED_EXPR;/map \\\$request_uri \\\$redirect_to_https/,/^}/d"
  SED_EXPR="$SED_EXPR;/map \\\$host \\\$is_blog_host/,/^}/d"
fi

先让站以 HTTP 活着,DNS 切过来之后 sudo blog tls issue 签发,再重新生成 vhost 把跳转恢复。能打开的 HTTP 站,比打不开的 HTTPS 站有用。

本地电脑被清空之后

这是这次改动的起因。宿主机会定期清空本地磁盘,存不住东西。

清空之后什么都不用抢救,源码在服务器。三条路:

一、直接在服务器上写(本来就是主路径)

ssh ubuntu@211.159.166.158
cd /home/ubuntu/cc/blog
./blog new "标题" --tags 折腾
./blog build && ./blog publish

二、临时拉回本地改

scp -r ubuntu@IP:/home/ubuntu/cc/blog ~/blog
cd ~/blog && ./blog build      # 包里的 hugo 在 .bin/,不用另外装

注意这条是临时的,不是双副本策略:本地那份用完即弃。真要留,就得承担两份源码互相漂移、还得手工同步的代价——那正是这套方案当初想甩掉的东西。

三、让 AI 从服务器重新接手

新会话里只要说清「博客在哪个 IP、哪个目录」,它 ssh 上去就能继续。前提是 SSH 口令存在密码管理器里——这是整条链上唯一不能被自动化的一环。

自动快照

服务器上配了 crontab,每周一凌晨 4 点打一次:

0 4 * * 1 cd /home/ubuntu/cc/blog && PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin ./blog snapshot >> snaps/cron.log 2>&1

那串 PATH= 不是多余的。cron 的环境极简,不给 PATH 的话脚本里调裸命令(python3、sqlite3、tar)会找不到,包会打成半截。这类问题手动跑永远复现不出来。

只留最近 5 个,约 560 MB。blog doctor 会检查关键资产齐不齐、上次快照距今多少天。

但这只防误删,不防机器没了。 快照和站点在同一块盘上。要真正安全,得每月把最新的包拉一份到别处——网盘、移动硬盘、另一台机器。包里有 isso.cfg 的 secret、TLS 私钥和公众号 AESKey,它就是钥匙本身,绝不能往公开的地方放,要传先 gpg -c 加密。

回滚

blog publish 每次都把旧站点打成 /www/backup/blog-public-<时间戳>.tar.gz:

sudo tar -xzf /www/backup/blog-public-20261113-101500.tar.gz -C /www/wwwroot/blog-static

改 nginx 前的配置备份在 /www/backup/vhost/。restore.sh 本身也守三条:不删旧目录、不改旧 vhost、不碰 DNS,所以回滚成本极低。

五条禁止事项

  1. 不要对现有 WordPress 根目录执行 rsync --delete。这条能直接毁掉一个站。旧目录虽已退役,但任何「同步」类命令都要先 --dry-run 看清楚它要删什么。
  2. 不要在删旧站前跳过备份与核对。旧 WordPress 目录已于 2026-11-13 删除,删之前逐张核对过图片(旧站 1307 张与新站 1335 张零缺口)、并把整目录与数据库完整备份到 backups/。教训是:「服务器上还有原件」这种感觉不能替代一次逐文件核对。现在那份备份的唯一副本就在 backups/,已在快照内。
  3. 不要把 admin-password.txt 打进任何包里。后台口令是明文(Isso 的 admin 登录就做明文比对,写哈希永远登不进去),但它不该跟着包走。
  4. 不要弄丢强制跳转的例外表。站点已强制 http → https,靠三张 map 放行 /.well-known/(ACME 续签)与 /mp-weixin/(公众号回调)。任何一条被吃掉, 要么证书到期后续不上,要么公众号接口静默出错。
  5. 不要下发 HSTS。证书链是 Let’s Encrypt 新层级,很老的 Android 7.1 以下 / 未更新的 Win7 可能不信任新根,加了 HSTS 且不可撤销,等于把老设备锁在门外。

一句话总结

以前换服务器是「列一张清单,逐项回忆还漏了什么」;现在是「blog snapshot,拷走,restore.sh」。

真正让这件事变简单的不是 Hugo,而是承认了一件事:散落在系统路径里的东西才是备份的真正难点——图片在 webroot、评论在 /var/lib、配置在面板目录、证书在面板的 cert 目录、公众号后端在站点里而非构建产物里、二进制在隐藏目录。把清单列全,剩下的都是 cp。

补充:这份手册后来补过的两处

本文最初写于 HTTPS 上线之前,包是 57 MB。之后补了两处,都是「换机当天才会发现」的类型:

  • 退役旧站备份进包。旧 WordPress 目录删除后,它的唯一副本在 backups/,而当时的快照用的是显式目录清单,没带上它——等于换机即丢失。已修正。
  • 证书与强制跳转的联动。见上文第六个坑。这是 HTTPS 上线后才出现的依赖关系。

现在包是 112 MB:多了证书、公众号后端、退役备份,以及新写的第 4 篇带来的内容。数字会随文章增长,但清单的结构不会变:源码、原件、图片、评论、配置、证书、后端、二进制。记住这个结构,比记住体积有用。


这个系列共四篇:迁移实录 · 写作与发布流程 · 换服务器手册 · HTTPS 上线记

加入讨论

新评论 — 条

评论保存在本站自托管服务里,无需注册登录,填个昵称就能留言;不依赖任何第三方 Cookie 或账号。请尽量说点有用的 :)