换服务器手册:一个包搬走整站
站点静态化之后,换服务器从「折腾两天」变成「拷一个文件、跑一个脚本」。
但前几版方案有个隐患一直没解决:备份包不含历史图片。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,所以回滚成本极低。
五条禁止事项
- 不要对现有 WordPress 根目录执行
rsync --delete。这条能直接毁掉一个站。旧目录虽已退役,但任何「同步」类命令都要先--dry-run看清楚它要删什么。 - 不要在删旧站前跳过备份与核对。旧 WordPress 目录已于 2026-11-13 删除,删之前逐张核对过图片(旧站 1307 张与新站 1335 张零缺口)、并把整目录与数据库完整备份到
backups/。教训是:「服务器上还有原件」这种感觉不能替代一次逐文件核对。现在那份备份的唯一副本就在backups/,已在快照内。 - 不要把
admin-password.txt打进任何包里。后台口令是明文(Isso 的 admin 登录就做明文比对,写哈希永远登不进去),但它不该跟着包走。 - 不要弄丢强制跳转的例外表。站点已强制 http → https,靠三张
map放行/.well-known/(ACME 续签)与/mp-weixin/(公众号回调)。任何一条被吃掉, 要么证书到期后续不上,要么公众号接口静默出错。 - 不要下发 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 篇带来的内容。数字会随文章增长,但清单的结构不会变:源码、原件、图片、评论、配置、证书、后端、二进制。记住这个结构,比记住体积有用。
加入讨论
新评论 — 条评论保存在本站自托管服务里,无需注册登录,填个昵称就能留言;不依赖任何第三方 Cookie 或账号。请尽量说点有用的 :)