SSD Nodes Learn 🎉 VPS $5.50/月起
指南 Matt Connor作者: Matt Connor

如何将服务器迁移到新的 VPS

按演练流程迁移 Linux 服务器到新 VPS:先清点并重建,分两次同步数据库和文件,提前降低 DNS TTL,验证新 IP 后再切换。

将服务器迁移到新的 VPS,并按演练流程完成切换

将服务器迁移到新的 VPS 时,应将这次迁移视为一次经过演练的切换,而不是简单复制。先从头构建新服务器,分两次同步数据;在修改 DNS 前,先确认新服务器可通过自己的 IP 地址独立运行;然后切换 DNS 记录,并让旧服务器继续运行,直到确认迁移没有问题。复制数据是最简单的部分。操作顺序决定了迁移是平稳完成,还是造成高昂代价。

本指南适用于一台运行 Web 应用、数据库和 TLS(传输层安全)证书的 Linux 服务器。这涵盖了大多数单服务器部署。迁移涉及两台主机,因此每个示例都会在注释中说明运行主机。地址使用文档专用地址范围:198.51.100.10 是旧服务器,203.0.113.20 是新服务器。

开始前请通读整份操作手册。第一步是降低 DNS TTL,这必须在真正执行切换的步骤前几天完成。

构建任何内容前,先完成清单

无法重建未记录配置的服务器。花一个小时记录旧服务器的用途,因为迁移后出问题的通常正是没人记得的内容:cron 任务、防火墙例外规则,或放在应用目录之外的环境文件。

在旧服务器上运行以下命令,并将输出保存到新服务器可以读取的位置。

# old server: packages you asked for, not the dependencies they pulled in
apt-mark showmanual > ~/inv-packages.txt

# old server: what runs now, and what starts at boot
systemctl list-units --type=service --state=running --no-pager > ~/inv-services.txt
systemctl list-unit-files --state=enabled --no-pager >> ~/inv-services.txt
systemctl list-timers --all --no-pager > ~/inv-timers.txt

# old server: what is listening, on which address and port
sudo ss -tulpn > ~/inv-ports.txt

# old server: human accounts, skipping the system ones
awk -F: '$3 >= 1000 && $3 < 65534 {print $1, $3, $6, $7}' /etc/passwd

apt-mark showmanual 是值得保留的列表,因为它会列出作为依赖项安装的所有内容。在一台使用了五年的服务器上执行完整的 dpkg --get-selections 会返回两千行结果,但无法说明安装这些内容的目的。

计划任务分布在两个位置,因此需要分别检查。每月才运行一次的任务,往往会在迁移六周后才被发现。

# old server: per-user crontabs, then the system drop-ins
for u in $(cut -d: -f1 /etc/passwd); do sudo crontab -lu "$u" 2>/dev/null | sed "s/^/$u: /"; done
sudo ls -la /etc/cron.d /etc/cron.daily /etc/cron.hourly

接下来检查不属于普通文件的部分:防火墙规则、证书、数据库,以及实际需要迁移的数据量。

# old server
sudo ufw status verbose        # or: sudo nft list ruleset
sudo certbot certificates
sudo -u postgres psql -c '\l'  # or: sudo mysql -e 'SHOW DATABASES;'
sudo du -xh --max-depth=1 / | sort -h

certbot certificates 会输出每个证书的名称、覆盖的域名、到期日期以及磁盘上的文件路径。这些输出就是 TLS 检查清单。du -x 只在同一个文件系统中统计,因此不会进入已挂载的备份卷,也不会返回大十倍的错误数值。

有两项内容位于服务器之外,而且每次都会被遗漏。第一,任何将服务器 IP 地址加入允许列表的第三方,例如支付网关、托管数据库、SMTP 中继或合作伙伴 API。新服务器使用新的地址,因此应在切换前将新 IP 加入这些允许列表,而不是等到切换后再处理。第二,检查并非由您创建的 DNS 记录,例如 MX 记录,或文本中包含旧 IP 的 SPF 记录。

为什么要重建,而不是克隆旧的 root 文件系统

将整个 root 文件系统克隆到新的 VPS 上看起来更快,实际上也确实更快,直到问题出现为止。已经在生产环境运行多年的 root 文件系统中,通常包含无人记录的手工编辑配置、来自已不存在的软件仓库的软件包,以及针对旧平台虚拟硬件构建的启动配置。您会把这些内容全部导入,其中也包括导致您必须迁移的原因。

重建在第一天会更慢,但之后每天都能降低维护成本。安装当前版本,应用基础加固配置,然后只复制数据:应用目录、站点配置、数据库转储、证书和用户上传文件。任何无法解释的内容都不应迁移。按照您启动任何服务器的方式启动新服务器,先完成新 VPS 上的前十分钟,然后根据清单逐个添加服务。确认一个服务正常后,再添加下一个。

当恢复镜像或快照是正确选择时

重建服务器有一个合理的例外。如果旧服务器无法启动,或者应用已无法再从源代码重新构建,那么恢复云服务商镜像或快照是务实的选择。但它有明确限制:恢复通常只能在同一云服务商内进行,而且往往仅适用于同一计划系列,因为恢复的磁盘依赖该平台的虚拟设备和网络命名方式。

运行中的服务器快照也会遇到与其他实时数据库文件级副本相同的一致性问题。应将镜像恢复视为恢复路径,而不是迁移方案。在基于快照制定方案前,请先阅读为什么快照不等同于备份

文件如何传输:通过 SSH 使用 rsync

在旧服务器上运行 rsync,将数据推送到新服务器。推送通常更简单,因为数据已经在旧服务器上,并且旧服务器可以通过 sudo 读取全部数据。

# old server: dry run first, and read what it says it will do
sudo rsync -aHAX --dry-run --itemize-changes \
  -e 'ssh -i /root/.ssh/id_ed25519' \
  /srv/app/ deploy@203.0.113.20:/srv/app/

# old server: the real bulk pass
sudo rsync -aHAX --info=progress2 \
  -e 'ssh -i /root/.ssh/id_ed25519' \
  /srv/app/ deploy@203.0.113.20:/srv/app/

这些选项很重要。-a 保留权限、时间戳、符号链接和所有权。-H 保留硬链接,而不是将其展开为多个独立副本。-A 复制 POSIX ACL(访问控制列表),-X 复制扩展属性。如果缺少最后两项,文件即使看起来完全相同,行为也可能不同,因为 SELinux 标签和 ACL 存储在扩展属性中,没有其他信息会记录它们。

这里的大多数故障都由以下两个细节导致。

末尾的斜杠决定数据的落点。 /srv/app/ 表示该目录的内容。/srv/app 表示目录本身。如果写错,新服务器上就会出现 /srv/app/app。应用启动后会报告缺少文件,因为配置中的路径现在少了一层。

sudo 中,波浪号表示 root 的主目录。sudo rsync 中写入 -e 'ssh -i ~/.ssh/id_ed25519' 时,程序会在 /root/.ssh 中查找密钥,而不是在您自己的主目录中查找。如果密钥不存在,SSH 会输出 Permission denied (publickey),rsync 会输出 rsync: connection unexpectedly closed 并以非零状态退出。请完整写出密钥路径。如果修正路径后仍反复出现该身份验证消息,请查看publickey 失败通常由少数几种原因导致,然后检查新服务器上的目录权限。

所有权需要做出一个选择。以 root 身份运行时,rsync 默认按名称映射所有者和组。因此,旧服务器上由 www-data 所有的文件,即使数值 UID(用户 ID)不同,也会在新服务器上归属于 www-data。重建服务器时,这通常正是所需结果。只有在复制的文件系统中的账户不存在于目标服务器时,才添加 --numeric-ids。然后使用 ls -ln 检查结果,因为由没有对应账户的 UID 所有的文件会显示为纯数字,读取这些文件的所有服务都会被拒绝访问。

提前几天执行批量同步,此时旧服务器仍在提供流量。您可以按需重复执行:rsync 只发送发生变化的数据,因此第二次同步只需几分钟,而不是几小时。在切换窗口内执行最后一次同步时,添加 --delete,这样旧服务器上已删除的文件也会从新服务器中删除。

# old server: final pass, inside the window, after the app has stopped writing
sudo rsync -aHAX --delete --info=progress2 \
  -e 'ssh -i /root/.ssh/id_ed25519' \
  /srv/app/ deploy@203.0.113.20:/srv/app/

--delete 会删除目标端存在但源端已不存在的文件。因此,错误的源路径配合 --delete 可能清空目标目录。每次执行前都先使用 --dry-run。长时间传输还会因笔记本电脑上的 SSH 会话断开而中断,因此请在旧服务器上的 tmuxscreen 中启动传输。如果复制过程占满链路,而旧服务器仍在为用户提供服务,请添加 --bwlimit=20M

数据库如何迁移:使用原生转储

数据库并不是一个文件目录,尽管它看起来像目录。数据库由文件、内存状态和预写式日志组成。只有在数据库定义的一致性时刻,这些内容才是一致的。请使用数据库自带的工具。

PostgreSQL 需要执行两次转储,因为角色是集群级对象,而 pg_dump 不包含角色:

# old server
sudo -u postgres pg_dumpall --globals-only -f /var/backups/globals.sql
sudo -u postgres pg_dump -Fc -f /var/backups/appdb.dump appdb
# new server: restore in this order
sudo -u postgres psql -f /var/backups/globals.sql
sudo -u postgres createdb -O appuser appdb
sudo -u postgres pg_restore -d appdb --no-owner /var/backups/appdb.dump

跳过 globals.sql 后,所有表都会恢复,但没有任何应用角色能够读取这些表,因为 GRANT 语句引用了不存在的用户。-Fc 写入自定义归档格式,该格式只能由 pg_restore 读取,并且支持稍后恢复选定的表。请恢复到相同或更高的主版本。向后恢复不受支持,例如不能从 17 恢复到 16;pg_restore 会在写入任何内容前,先从文件头发现不支持的版本,并拒绝该归档。

MySQL 和 MariaDB 使用一条命令,并需要显式启用以下四个默认关闭的选项:

# old server
sudo mysqldump --single-transaction --routines --triggers --events \
  --databases appdb > /var/backups/appdb.sql
# new server
sudo mysql < /var/backups/appdb.sql

--single-transaction 会创建一致性快照,而不会阻塞写入操作,但这只适用于 InnoDB 表。同一数据库中的 MyISAM 表不会获得此保证,因此在信任转储前,请检查存储引擎。默认情况下,--routines--triggers--events 均处于关闭状态。这意味着普通转储会恢复数据,但会静默遗漏存储过程和计划事件。数据库用户及其授权存储在 mysql 系统数据库中,而 --databases appdb 转储不会处理该数据库。因此,请在新服务器上使用 CREATE USERGRANT 重新创建这些用户和授权。MariaDB 11 提供的同一工具名为 mariadb-dump,并保留 mysqldump 作为符号链接,因此截至 August 2026,两个名称都可以使用。

SQLite 使用单个文件。如果应用正在写入时复制该文件,得到的文件可能不完整。SQLite 提供了安全的操作方式:

# old server
sqlite3 /var/lib/app/app.db ".backup '/var/backups/app.db'"

无论使用哪种数据库引擎,都应在信任转储前检查它。磁盘空间耗尽可能导致转储提前停止,而恢复时可能不会报错,直到恢复过程执行到被截断的位置。

# old server: a complete mysqldump ends with a line reading "-- Dump completed on ..."
tail -n 3 /var/backups/appdb.sql

# new server: after restore, count rows in a table whose size you know
sudo mysql -e 'SELECT COUNT(*) FROM appdb.orders;'

为什么不能对运行中的数据库使用 rsync

rsync 按文件复制。运行中的数据库会同时写入多个文件,因此等 rsync 处理完最后一个文件时,第一个文件的内容可能已经过期。这样得到的副本包含来自不同时间点的数据页,数据库从未处于这种状态。结果可能是服务器拒绝启动;更糟糕的情况是,服务器能够启动并连续一周返回正确结果,直到某个查询访问损坏的数据页后才失败。期间不会出现任何警告。

安全移动文件本身有两种方式。停止数据库,完成复制后再启动:结果正确、过程简单,但停机时间等于复制所需的时间。另一种方式是使用专为复制运行中服务器的物理数据而设计的工具。对于 PostgreSQL,该工具是 pg_basebackup。它会与服务器协调,确保副本一致:

# new server: pull a physical copy from the old one
pg_basebackup -h 198.51.100.10 -U replicator -D /var/lib/postgresql/16/main -X stream -P

这要求使用具有 REPLICATION 属性的角色,并在旧服务器上配置匹配的 pg_hba.conf 条目,因此准备工作比导出更复杂。当数据库足够大、导出和恢复无法在维护窗口内完成时,这种方式值得采用。对于普通的单服务器迁移,导出通常更合适。

在切换前重新签发证书,不要等到切换后

TLS 证书与域名绑定,而不是与 IP 地址绑定,因此证书文件本身可以直接迁移。无法顺利迁移的是续期过程。Certbot 默认的 HTTP-01 challenge 要求证书颁发机构通过端口 80,访问待签发证书的域名来获取文件。在 DNS 指向新服务器之前,该请求会到达旧服务器,导致新服务器上的续期失败。

第一种方法是复制现有证书及其续期状态。无论证书位于哪台服务器上,在到期日期之前都保持有效。

# old server
sudo rsync -aHAX -e 'ssh -i /root/.ssh/id_ed25519' \
  /etc/letsencrypt/ root@203.0.113.20:/etc/letsencrypt/

/etc/letsencrypt/renewal/下的每个文件都会记录签发证书时使用的验证器插件。因此,请在新服务器上安装相同的插件(例如 python3-certbot-nginx),否则第一次续期会因验证器未知而失败。请先验证续期能够正常工作,再依赖自动续期:

# new server, after DNS has moved
sudo certbot renew --dry-run

第二种方法是在新服务器上使用 DNS-01 challenge 签发新证书。该验证方式通过 TXT 记录证明域名控制权,不会访问端口 80。迁移前即可执行此操作,因为此时域名仍解析到旧服务器。如果您可以自动化管理 DNS 服务商,这通常是更稳妥的选择。使用 DNS-01 challenge 签发证书介绍了插件和凭据的配置方法。

无论采用哪种方法,都应在不修改 DNS 的情况下,检查新服务器实际提供的证书:

# your laptop
echo | openssl s_client -connect 203.0.113.20:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -subject -dates -issuer

-servername会发送 SNI(服务器名称指示),Web 服务器据此选择正确的虚拟主机。省略该参数时,服务器会返回该 IP 对应的默认证书,从而产生证书不匹配。这看起来像是真正的问题,但实际并非如此。

切换前提前数日降低 DNS TTL

DNS 是谨慎的迁移仍可能出错的环节,因为延迟是内置的,无法在切换当天缩短。解析器缓存您的 A 记录后,会在记录指定的 TTL(生存时间)内继续提供该记录。如果解析器在 10 分钟前按旧值缓存了记录,现在降低 TTL 对它不起作用:它会在旧 TTL 的剩余时间内继续使用旧值,之后才会获知新的较短 TTL。因此,至少要提前一个完整的旧 TTL 周期降低 TTL。提前一天是更稳妥的做法。如果您不熟悉这里涉及的流程,请参阅记录、解析器和缓存的说明

下面的数字根据 TTL 本身计算得出,不是测量结果。

ChartHow long a stale IP can survive, by record TTL
The data behind this chart
[
  {
    "label": "TTL 3600 s (common default)",
    "ttl_seconds": 3600,
    "worst_case_stale_minutes": 60
  },
  {
    "label": "TTL 900 s",
    "ttl_seconds": 900,
    "worst_case_stale_minutes": 15
  },
  {
    "label": "TTL 300 s (lowered for cutover)",
    "ttl_seconds": 300,
    "worst_case_stale_minutes": 5
  },
  {
    "label": "TTL 60 s (short window)",
    "ttl_seconds": 60,
    "worst_case_stale_minutes": 1
  }
]

TTL 为 3600 秒的记录,在您修改记录后,最长可能继续将用户发送到旧 IP 60 分钟。将 TTL 降低到 300 秒后,最长延迟会降至 5 分钟。应将这些数字视为最低限度,而不是保证值。部分解析器会自行设置最小 TTL,并忽略更短的值;部分应用运行时会在进程的整个生命周期内缓存解析出的地址。因此,在您修改记录前已启动的客户端,可能直到重启后才会再次查询。

确认较低的 TTL 已生效时,应读取权威应答,而不是读取您自己的缓存:

# your laptop: ask the zone's own nameserver, so no cache is involved
dig +short NS example.com
dig +noall +answer @$(dig +short NS example.com | head -n1) example.com A

该应答行的第二个字段是以秒为单位的 TTL。然后检查容易遗漏的记录:如果旧服务器有 IPv6,检查 AAAA 记录;如果 www 名称是单独的 A 记录而不是 CNAME,检查该记录;检查指向服务器自身的 MX 记录、列出旧 IP 的 SPF 记录,以及新地址上的反向 DNS(PTR)记录。如果服务器发送邮件,请在切换前通过服务提供商的控制面板设置 PTR,因为收件邮件服务器会检查该记录。缺少 PTR 可能导致邮件在其他配置看似正常数小时后仍被拒收。

在修改 DNS 前通过新服务器的 IP 验证服务器

DNS 仍指向旧服务器时,您可以在新服务器上测试整个应用。仅为一次请求覆盖名称解析:

# your laptop: force one name to the new IP, for this request only
curl -sS --resolve example.com:443:203.0.113.20 \
  -o /dev/null -w '%{http_code} %{ssl_verify_result}\n' https://example.com/

--resolve只会改变连接的目标位置。TLS 证书仍会根据真实名称进行验证,因此这同时验证了证书和服务。验证证书链后,%{ssl_verify_result}会输出0

如果要在浏览器中逐步操作网站,请在笔记本电脑的/etc/hosts中添加一行,为整台计算机覆盖名称解析;Windows 则修改C:\Windows\System32\drivers\etc\hosts

203.0.113.20 example.com www.example.com

然后按照用户的操作方式测试应用。登录。加载一个从数据库读取数据的页面。提交一个会写入数据库的表单。上传文件,并确认文件已写入磁盘。触发发送电子邮件的功能,并确认邮件已收到,因为新 IP 发起的出站 SMTP 连接经常会带来意外问题。完成后立即删除 hosts 配置行。保留该行会导致您花费一小时调试一个其他用户都能正常访问的网站。

切换流程

  1. 提前几天:降低 TTL,执行批量 rsync,配置新服务器,并通过 hosts 覆盖进行测试。
  2. 切换当天、维护窗口开始前:将新 IP 添加到所有第三方 allowlist,并确认新服务器已配置备份任务,且备份目标指向您的存储库。
  3. 打开维护窗口:在旧服务器上启用应用的维护模式,使其停止接受写入。
  4. 执行最终数据库转储,然后使用 --delete 执行最后一轮 rsync。
  5. 在新服务器上恢复转储并启动服务。
  6. 通过 --resolve 和 hosts 覆盖再次测试,包括执行一次真实写入。
  7. 将 A 和 AAAA 记录修改为新 IP。
  8. 监控两台服务器。旧服务器的访问日志会显示仍在访问旧服务器的客户端,数量应在 TTL 时间内逐渐降至 0。
  9. 关闭维护页面。
  10. 让旧服务器继续运行,至少 1 周内不要对其进行任何修改。

维护模式这一步最容易被跳过,但它正是保护措施。新数据库接受写入后,回滚要么会丢失这次写入,要么需要从新数据库导出数据,再将其导入旧数据库。几分钟的只读窗口成本很低。两台数据库都已接受写入后,则需要数天进行人工对账。

回滚方案

回滚只有一个操作:将 DNS 记录改回 198.51.100.10。它之所以可行,是因为您已提前完成以下四项工作。

  • 旧服务器仍在运行,服务正常,数据完整。您只是停止了向其写入数据,并未将其下线。
  • TTL 仍然较低,因此回退速度与之前切换过去的速度相当。
  • 您将新 IP 添加到第三方允许列表中,而不是替换旧 IP。如果删除旧地址,支付网关处的回滚路径就会失效。
  • 新服务器没有产生任何您无法识别的写入,因为到目前为止,唯一的写入都是您自己的测试事务。

在维护窗口开始前,先确定触发回滚的条件。设置两个条件就足够:任何在固定分钟数内无法诊断的错误,以及任何数据丢失。提前写下这些条件,才能避免因反复猜测而耗费一小时,最终让原本十分钟的中断变得更长。

验证迁移是否成功

网站可以加载,并不表示迁移已经完成。还要检查那些只会在之后才失败的部分。

# new server
systemctl --failed
journalctl -p err -b --no-pager | tail -n 40
sudo certbot certificates
sudo systemctl list-timers --all --no-pager

systemctl --failed reporting 0 loaded units listed is the result you want. certbot certificates 应显示预期的到期日期,list-timers 应显示清单中的每个计划任务及其实际的下次运行时间,不能留空。

然后,在监控服务的情况下,有意重启一次新服务器。手动启动但从未启用的服务,在凌晨 3 点第一次意外重启之前,通常都能正常运行。

# new server
sudo reboot

# your laptop, once it comes back
curl -sS -o /dev/null -w '%{http_code}\n' https://example.com/

如果应用运行在容器中,问题表现会有所不同,因为Compose 堆栈需要明确的重启策略,才能在重启后恢复运行

最后一项检查最容易被推迟,但也最重要:备份任务。以未备份的服务器结束迁移,只是将一种风险换成了另一种风险。手动在新服务器上运行备份,然后将其中一个文件恢复到临时目录。实际测试过恢复操作的 restic 仓库,才是在需要时真正有用的备份。 如果旧服务器和新服务器并行运行一周,以一致的方式访问和配置每台主机,可以避免两台服务器同时在线时逐渐产生配置差异。

切换完成后:旧服务器和最后几项工作

保留旧服务器一到两周。费用只是您原本准备取消的套餐再使用一个月,但它是您唯一的回滚手段。然后完成其余收尾工作。

  • 在新的 ~/.ssh/config 中复用相同的主机名后,首次连接时会出现 WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!,因为该名称现在返回了不同的主机密钥。确认主机密钥为何发生变化后,再使用 ssh-keygen -R example.com 清除过期条目,不要条件反射式地执行,因为相同的警告也可能表示存在拦截攻击。迁移也是审查密钥访问范围的好时机,包括哪些密钥可以访问哪些资源;小型服务器集群中的 SSH 密钥管理正是用于此目的。
  • 对旧服务器执行最后一次快照或备份,并将其存储在旧服务商以外的位置。
  • 按此顺序,并作为最后一项工作,从监控检查、SPF 记录和第三方允许列表中移除旧 IP。
  • 只有确认最终副本已在其他位置成功读取后,才取消旧套餐。

FAQ

将服务器迁移到新的 VPS 需要多长时间?

用户可感知的中断通常只包括最终数据库转储、最后一次 rsync 传输和启动服务,因此小型应用通常需要十到三十分钟。整个迁移所需的日历时间更长,因为切换前至少要提前一个旧 TTL 周期降低 DNS TTL,提前一天更稳妥。也应尽早规划批量数据复制,最好提前几天开始。批量复制可在服务器运行期间进行,之后再次执行时只会传输上次复制后发生变化的数据。

可以在 MySQL 或 PostgreSQL 运行期间使用 rsync,而不进行转储吗?

不可以。rsync 按文件复制数据,而数据库会同时写入多个文件,因此复制结果中的页面来自不同时间点,代表数据库从未处于过的状态。数据库可能拒绝启动,也可能启动后在查询访问损坏页面时才失败。请使用 pg_dumppg_dumpall --globals-only,或使用 mysqldump --single-transaction;也可以先停止数据库,再复制文件。对于大型 PostgreSQL 集群,pg_basebackup 可以从运行中的服务器创建一致的物理副本。

修改 DNS 前,如何测试新的 VPS?

在您自己的计算机上覆盖名称解析。对于单个请求,curl --resolve example.com:443:203.0.113.20 https://example.com/ 会将连接发送到新的 IP,同时仍根据真实名称验证证书。要测试浏览器访问,请在笔记本电脑上的 /etc/hosts 中添加 203.0.113.20 example.com,依次完成登录、数据库读取、表单写入和文件上传,然后删除该行。要单独检查证书,请运行 openssl s_client -connect 203.0.113.20:443 -servername example.com

应设置什么 TTL?应在什么时候降低 TTL?

将 A 和 AAAA 记录的 TTL 降低到 300 秒,并至少在切换前一个完整的旧 TTL 周期执行。解析器如果在修改前缓存了记录,就会在剩余的旧 TTL 时间内继续使用旧值。因此,如果旧 TTL 为 86400,提前一小时降低 TTL 不会产生任何效果。迁移完成几天后,确认旧服务器的访问日志已经没有新请求,再将 TTL 恢复为平常的值。

应该复制 TLS 证书,还是在新服务器上重新签发?

两种方式都可以。复制 /etc/letsencrypt/ 可以让证书继续有效到原定到期时间,但您必须在新服务器上安装相同的 certbot 验证器插件,否则首次续期会失败。因此,切换 DNS 后运行 certbot renew --dry-run 进行确认。如果可以使用 DNS-01 challenge,重新签发通常更简单,因为它通过 TXT 记录证明控制权,并且在 DNS 指向新服务器前即可工作。DNS 指向新服务器前不能在新服务器上使用 HTTP-01 challenge,因为验证请求会到达旧服务器。