网站换服务器实录:备份四件套、字符集陷阱与五个必须改的域名字段

一次迁移事故:文章都还在,但用户全被踢下了线

去年帮人把一个跑了三年的 Typecho 站从一台 1G 内存的旧 VPS 挪到新机器上,整个过程我自认为很标准:tar 打包网站目录,mysqldump 导出数据库,新机器上装好 PHP 和 Nginx,把域名解析改过去。前后不到四十分钟,页面打开正常,文章列表正常,图片正常,我以为收工了。

第二天朋友打电话来,说后台进不去了,登录之后跳回登录页,反复输密码也没用。这就是本文要讲的第一个坑,也是最容易被忽略的一个:网站迁移里最麻烦的从来不是文件,而是那些藏在数据库和配置文件里的、与服务器环境绑定的东西。

这篇文章按迁移的真实顺序走一遍:先备份、再在新机准备环境、然后迁移数据、最后处理五个「域名字段」和一批必须手工核对的项目。每一步都给出可执行的命令和验证方法。这篇文章写给一个人维护一台服务器的站长,不讲集群、不讲零停机,就把一台机器搬到另一台机器这件事做干净。

第一步:备份不是 cp,是三个独立的部分

很多人说的「备份网站」就是 cp -r /var/www /backup,这个动作只覆盖了文件,漏掉了数据库、cron 任务、证书和一些系统级配置。真正的迁移备份至少是四份东西,每份单独存、单独校验:

# 1) 网站文件(注意用 tar 保留权限和属主,别用 cp -r)
tar -czpf /backup/site-$(date +%F).tar.gz -C /var/www/html .

# 2) 数据库(单事务导出,保证一致性)
mysqldump -uroot -p --single-transaction --quick \
  --default-character-set=utf8mb4 zz1984 \
  > /backup/db-$(date +%F).sql

# 3) 配置文件(Nginx 站点配置、PHP 配置、SSL 证书、cron)
tar -czpf /backup/conf-$(date +%F).tar.gz \
  /etc/nginx /etc/php /etc/letsencrypt /var/spool/cron 2>/dev/null

# 4) 环境清单(装了哪些包、哪些服务、哪些版本)
dpkg -l | awk '/^ii/{print $2"\t"$3}' > /backup/packages.txt
systemctl list-units --type=service --state=running --no-pager > /backup/services.txt
php -v > /backup/versions.txt; nginx -v 2>> /backup/versions.txt; mysql --version >> /backup/versions.txt

第四份「环境清单」是这里面最容易被跳过、事后最救命的一份。迁移完发现某个功能不工作,第一反应往往是对比两边的软件版本。packages.txt 让你能一条 diff 看出新机上少了什么 PHP 扩展,不用凭记忆猜。

# 备份完立刻校验,别看文件存在就放心
ls -lh /backup/
gzip -t /backup/site-$(date +%F).tar.gz && echo "site ok"
tail -3 /backup/db-$(date +%F).sql        # 结尾应该有 Dump completed
grep -c 'CREATE TABLE' /backup/db-*.sql    # 表数量对不对,心里要有数

mysqldump 结束时会输出一行 -- Dump completed on ...。如果没有这一行,说明导出中途断了,这个文件不能用。这是个十秒钟就能做的检查,我看过有人拿着一个截断的 SQL 文件导了半小时,最后报语法错误,白白浪费一晚上。

第二步:新机先准备环境,别等数据到了才发现缺东西

正确的顺序是:先在目标机器上把环境准备好、跑通一个静态页面,再把数据搬过来。反过来做的话,一旦出问题,你分不清是数据的问题还是环境的问题。

# 按 packages.txt 对齐版本,重点是 PHP 版本一致
# Typecho on PHP 5.6 这类老站,千万不能顺手装成 PHP 8
php -v      # 旧机
php -v      # 新机(必须与旧机同一大版本)

# 对比扩展列表,这是「页面空白但没报错」的头号原因
# 旧机执行
php -m | sort > /tmp/ext-old.txt
# 新机执行
php -m | sort > /tmp/ext-new.txt
diff /tmp/ext-old.txt /tmp/ext-new.txt
# 只在新机有:不用管
# 只在旧机有:必须装,否则依赖它的功能会静默失败

PHP 扩展缺失的表现非常阴险。轻则某个函数报「Call to undefined function」,重则整个页面白屏、日志里什么也没有,因为显示错误被关掉了。所以迁移之后第一件事应该是打开临时错误日志:

# 在站点配置里临时打开错误日志(不要开 display_errors)
php_admin_flag display_errors off
php_admin_value log_errors on
php_admin_value error_log /var/log/php/site-error.log
# 确认目录可写
mkdir -p /var/log/php && chown www-data:www-data /var/log/php
tail -f /var/log/php/site-error.log

另外,Nginx 的 server_name 也要在新机上先配好,用 curl -H 'Host: www.example.com' http://127.0.0.1/ 验证新机能正确响应,这一步在切 DNS 之前做,等于给自己留了一条随时能回退的后路。

第三步:两个虚拟主机的配置差异,往往藏在这里

「旧机能跑新机不能跑」的问题,八成出在 Nginx 配置的细节上。迁移前把旧机的全部 include 配置拉一遍,重点看这几项:

配置项迁移时的坑检查方式
root 路径旧机 /home/wwwroot/site,新机 /var/www/sitenginx -T \| grep root
fastcgi_pass用 unix socket 还是 127.0.0.1:9000ls -l /run/php/*.sock
rewrite / try_files伪静态规则漏了,内页全部 404点开三个内页看是否正常
文件权限新机 www-data 的 UID 与旧机不同ps aux \| grep php-fpm 看实际用户
上传大小client_max_body_size 默认 1M后台上传一张 3M 图试试

文件和权限这一段要特别小心。用 tar 打包时如果没带 -p,属主和权限会丢;带上了 -p,解包后属主是数字 UID,如果新机上这个 UID 对应的用户不一样(比如旧机 www-data 是 33,新机也是 33,但有的发行版是 48),那么解出来的文件会归到一个陌生用户下,PHP 读写就失败了。检查方式很直接:

# 看看站点目录到底归谁
ls -ln /var/www/html | head -5      # -n 显示数字 UID
id www-data                          # 当前 www-data 的 UID
# 如果不一致,统一改回来(按目录和文件分别设权限)
chown -R www-data:www-data /var/www/html
find /var/www/html -type d -exec chmod 755 {} \;
find /var/www/html -type f -exec chmod 644 {} \;
# 只给必须可写的目录开写权限,别图省事 -R 777
chmod -R 775 /var/www/html/usr/uploads

顺便说一句,chmod -R 777 是迁移后最常见的「修复」手段,也是最糟的一个。它确实能让问题消失,但同时把你站上所有文件变成了任何进程都能改。如果之后被挂马,攻击者可以改你的 PHP 文件。宁可多花十分钟定位到底是哪个目录需要写权限,也不要一键 777。

第四步:数据库迁移,字符集是最大的拦路虎

导出的 SQL 文件搬到新机之后,导入之前先做两件事:看字符集,看表数。

# 导入前
head -20 /backup/db-2026-11-11.sql | grep -i 'character_set\|SET NAMES'
grep -c 'CREATE TABLE' /backup/db-2026-11-11.sql

# 新机上如果还没建库,要先建对字符集
mysql -uroot -p -e "CREATE DATABASE zz1984 \
  DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"

# 导入(加 --default-character-set 防止中文变问号)
mysql -uroot -p --default-character-set=utf8mb4 zz1984 \
  < /backup/db-2026-11-11.sql

# 导入后立刻核对表数,必须与导出时一致
mysql -uroot -p zz1984 -e "SELECT COUNT(*) FROM information_schema.tables
  WHERE table_schema='zz1984';"

字符集问题的典型症状是:文章里的中文全变成了 ???? 或者一串乱码。原因通常是旧库是 utf8(MySQL 里的 utf8 其实是最多 3 字节的 utf8mb3),新机默认建库是 utf8mb4,导入时客户端的连接字符集没指定,两边理解不一致。判据是看一个具体的中文标题:

mysql -uroot -p zz1984 -e "SELECT cid,title FROM typecho_contents LIMIT 3;"
# 如果中文正常显示,但网页上是问号 → 连接字符集问题(配置项)
# 如果这里就已经是问号 → 数据在导入时损坏了(要从备份重导)

这两者的修法完全不同。前者改 Nginx/PHP 的字符集配置就行,数据本身没坏;后者必须从备份重新导入,而且不要再尝试「修」,因为一个问号对应的原始字符已经丢失,修不回来。所以导入之后、切 DNS 之前,一定打开一篇带中文的文章看一眼。

第五步:五个「域名字段」,不改就等着掉登录

现在回答开头那个事故。Typecho、WordPress 这类程序都会把站点地址存进数据库,因为它需要知道自己的绝对路径来生成链接。换了服务器通常没问题,但换了域名(或者从 http 切到 https),这些字段里还是旧值,于是出现了「登录成功但立刻掉线」——因为 Cookie 是种在旧域名下的,或者跳转地址指向旧域名。

Typecho 的两个关键字段在 typecho_options 表里:

mysql -uroot -p zz1984 -e \
  "SELECT name,value FROM typecho_options WHERE name IN ('siteUrl','theme');"

# 换域名时改 siteUrl(注意结尾不要带斜杠)
mysql -uroot -p zz1984 -e \
  "UPDATE typecho_options SET value='https://www.example.com' WHERE name='siteUrl';"

改完要清一次缓存(Typecho 没有对象缓存,但浏览器端的 Cookie 要清),然后用无痕窗口登录测试。除了 siteUrl,迁移时还要逐项核对这四类「域名字段」:

位置内容怎么找
数据库选项表siteUrl、首页地址SELECT * FROM typecho_options
文章正文图片和链接里的绝对地址UPDATE ... REPLACE(value,'old.com','new.com')
配置文件伪静态、Sitemap 里的域名grep -r 'old.com' /var/www/html
第三方设置统计、搜索、CDN 里登记的回调域名逐个后台核对
# 文章正文里的绝对地址批量替换(先备份,先 SELECT 看影响行数)
mysql -uroot -p zz1984 -e "SELECT COUNT(*) FROM typecho_contents
  WHERE text LIKE '%old.example.com%';"
mysql -uroot -p zz1984 -e "UPDATE typecho_contents
  SET text = REPLACE(text,'old.example.com','new.example.com')
  WHERE text LIKE '%old.example.com%';"

# 文件里残留的旧域名
grep -rn 'old\.example\.com' /var/www/html --include='*.php' --include='*.html' | head

绝对地址的替换务必先 SELECT COUNT 看影响面。有次我直接跑 UPDATE,把一篇讲「怎么配置域名」的教程里的示例域名也替换了,文章里的代码示例变成了自己站的域名,读者照着敲会出问题。这类「文档里的示例」是替换的高危区,看到计数异常大时先抽查几篇。

第六步:切换前后,一份十项检查单

DNS 切换是不可逆的(有缓存,回退要等 TTL),所以在切换之前应该把新机验证到位。下面这份清单是我自己每次迁移都会过一遍的:

# 在新机上,指定 Host 头访问,模拟域名已切过来的效果
H=www.example.com
curl -sI -H "Host: $H" http://NEW_IP/ | head -1                     # 200
curl -sI -H "Host: $H" http://NEW_IP/archives/1.html | head -1      # 内页不 404
curl -s  -H "Host: $H" http://NEW_IP/ | grep -o '<title>.*</title>' # 标题正确
curl -sI -H "Host: $H" http://NEW_IP/sitemap.xml | head -1          # sitemap 可访问
curl -sI -H "Host: $H" http://NEW_IP/admin/login.php | head -1      # 后台可达
# 后台真正登录一次(用无痕窗口,走完登录 → 发文 → 退出全流程)
  1. 首页、内页、搜索页、分类页、标签页各点一遍,中文显示正常
  2. 后台上传一张图片,确认能写入并显示
  3. 发一篇测试文章,确认能保存、能访问
  4. PHP 错误日志为空(或只有无害的 warning)
  5. Nginx error.log 里没有 file not found
  6. 数据库表数量与旧机一致
  7. cron 任务已迁移(crontab -l 对比两边)
  8. SSL 证书已就位,curl -vI https://... 2>&1 | grep expire 看过期时间
  9. 定时备份脚本已改到新机的路径
  10. 旧机暂时不关机,保留至少一周作为回退

第 10 条值得强调。很多人切完 DNS 就顺手把旧机释放了,结果一周后发现某张图片 404、某个链接指向旧机、某个 CDN 回源还指着旧 IP。旧机保留一周的成本不过几块钱,但它是你唯一的退路。真要释放,先把它的磁盘做成镜像快照存着。

迁移后的收尾:三件小事,影响不小

切完之后还有三件事容易被忘。第一是搜索引擎:换 IP 对 SEO 影响很小,但如果换了域名,一定要在 Search Console 里做地址变更,并且把全站的 301 重定向配好(旧域名所有路径 → 新域名对应路径),这个 301 至少要保留一年。第二是监控:如果有 Uptime 检测或告警,检查目标地址是不是还指着旧 IP。第三是统计工具:看新机上线的头两天,访问量是不是突然掉了一半——那通常不是流量丢了,而是统计代码里的某个域名还指着旧站。

最后回到那个掉登录的事故。真正的原因是他换了域名,siteUrl 还停在旧域名上,登录后跳转的地址是旧域名,而旧域名已经解析到新机、Cookie 域不匹配,于是循环跳回登录页。一条 UPDATE 就解决了。但这个坑绕了整整一天,因为当时的思路是「数据库肯定没问题,我都是原样导的」——数据确实没问题,有问题的是数据里那个指向旧世界的地址。迁移的本质不是搬文件,而是把一台机器的身份完整地复刻到另一台上,凡是记录了这个身份的地方,都要一起搬。

Last modification:November 11th, 2026 at 07:25 pm

Leave a Comment