云服务器更换到阿里云:当前站点迁移、新机部署、数据迁移与应用切换
2026 年 10 月 30 日,我购买了一台阿里云服务器,配置为 2 核、2 GiB 内存、3 Mbps 带宽,系统使用 Ubuntu 24.04。旧云服务器还有一个月到期,这次迁移要把网站、中间件、附件和相关任务完整搬过去,同时保留家中小主机上的 MySQL 从库和附件备份。
我的个人网站可以接受短暂停写,因此没有设计双写或自动故障转移。迁移顺序定为:旧站继续运行时准备新环境并预同步文件;切换当天暂停写入,迁移 MySQL 和最后一批文件;应用验证通过后切换 DNS。
目前业务已经切到新云。MySQL 主从同步、旧附件访问、搜索和主要站点已经验证;新附件上传、日志实际轮转和整机重启恢复仍列在收尾清单中。
目录
- 迁移前后是什么样子
- 先确定迁移边界和顺序
- 准备新服务器
- 安装 Docker 和中间件
- 安装 Nginx、证书和日志策略
- 在停写之前预同步文件
- 切换 MySQL 主库
- 最终同步文件并发布应用
- 调整备份来源并切换 DNS
- 验证结果与收尾
- 下次迁移如何复用
1. 迁移前后是什么样子
1.1 迁移前的环境
旧云运行 Ubuntu 20.04,配置同样是 2 核、2 GiB 内存。博客和附件服务由家中小主机的 Jenkins 构建镜像,再通过 SCP 传到云服务器,最后用 docker run 发布。
旧环境的中间件比较分散:MySQL、Redis 和 Meilisearch 直接安装在宿主机,Nginx 也在宿主机;家中小主机是 MySQL 从库,每天还会拉取云端附件。Java 应用在 Docker 中访问宿主机中间件,使用过 host.docker.internal,Meilisearch 甚至经过 Nginx 的 84 端口代理。
这套环境一直能用,但下次迁移仍要重新安装、重新找配置。于是这次顺手做了三项整理:
- MySQL、Redis、Meilisearch 改为 Docker Compose 管理,配置和数据按应用保存。
- Java 应用保留现有 Jenkinsfile 和
docker run,不在迁移期间同时改发布方式。 - 新机清单、脚本、Compose、Nginx 配置和迁移记录统一放进
mine-linux仓库。
1.2 迁移后的连接关系

图中主从同步、附件备份的箭头表示数据流向,连接由小主机主动发起。
2. 先确定迁移边界和顺序
2.1 哪些数据要搬
- MySQL:迁移
lcxm_backend、lcxm_attachment两个业务库,并把家中从库接到新主库。 - 附件和静态文件:先预同步,停止写入后再做一次最终同步。
- Redis:不保留旧数据,新环境重新开始。
- Meilisearch:不迁移索引,由博客应用重新建立。
- Java 应用:重新构建镜像并发布,不复制旧容器运行目录。
- Nginx:使用整理后的配置,不直接照搬旧云全部历史文件。
- 证书:通过 acme.sh 重新签发,不复制旧证书状态。
2.2 准备阶段和切换阶段分开
准备阶段不影响旧站:
- 初始化新服务器,安装 Docker。
- 部署并验证 MySQL、Redis、Meilisearch。
- 配置 WireGuard、安全组、UFW、Nginx 和证书。
- 从旧云预同步附件和静态文件。
- 准备 Jenkins 到新云的 SSH 登录和发布地址。
切换阶段才暂停写入:
- 停止旧云和小主机中可能写业务库的应用或任务。
- 等家中 MySQL 从库追平旧主库。
- 导出数据库,导入新云,并把家中从库接到新主库。
- 最终同步附件和静态文件。
- 发布博客、附件应用,重建搜索索引。
- 验证新云后切换 DNS,再恢复写入和备份任务。
数据库安装和数据迁移特意分开。MySQL 容器可以提前启动和验证;真正的数据导入留在停写窗口中完成。
3. 准备新服务器
以下命令以 Ubuntu 24.04、root 账号为例。项目保存在 /xqd/infra/mine-linux/,不采用这个路径时,相应修改命令中的项目位置。
3.1 目录和全局变量
服务器上的资源统一放在 /xqd:
text
/xqd/
├── infra/mine-linux/ 脚本、配置样例、文档和迁移记录
├── docker/
│ ├── mysql/ compose.yaml、config/、data/
│ ├── redis/ compose.yaml、config/、data/
│ └── meilisearch/ compose.yaml、config/、data/
├── attachments/ 附件文件
└── server/ Java 日志与静态站点
仓库 server/docker/ 下的 mysql、redis、meilisearch 目录复制到 /xqd/docker/,保留各自的 compose.yaml 和 config/。data/ 是容器启动后产生的运行数据,不随项目复制,也不提交 Git。
仓库中的全局变量文件是无凭证样例。复制到 /etc/profile.d 后,填写服务器自己的密码和阿里云 DNS 凭证:
bash
cd /xqd/infra/mine-linux/server
cp config/xqd-global-env.sh /etc/profile.d/xqd-global-env.sh
chmod 600 /etc/profile.d/xqd-global-env.sh
vi /etc/profile.d/xqd-global-env.sh
source /etc/profile.d/xqd-global-env.sh
文件中主要有这些变量:
NEW_HOSTNAME:Linux 系统主机名。NEW_SERVER_HOST:SSH/SCP 使用的地址,这次是infra.xuqiudong.cn。DOMAIN:申请主域名和泛域名证书时使用。MYSQL_*、REDIS_*、MEILI_*:Compose 使用的镜像、端口、密码和密钥。Ali_Key、Ali_Secret:acme.sh 的阿里云 DNS 插件使用。
文件权限为 600,供后续 root 登录 shell 加载。当前会话要手动 source;cron、systemd 和已经运行的容器不会自动继承这些变量。Compose 只会把 environment 中明确声明的值传入容器。
查看时可以只列变量名,避免把凭证复制到终端记录中:
bash
printenv | cut -d= -f1 |
grep -E '^(NEW_HOSTNAME|NEW_SERVER_HOST|DOMAIN|MYSQL_|REDIS_|MEILI_|Ali_)'
3.2 系统基础设置
时区、字符集和主机名先处理:
bash
timedatectl set-timezone Asia/Shanghai
timedatectl
locale charmap
hostnamectl set-hostname "$NEW_HOSTNAME"
hostnamectl --static
locale charmap 返回 UTF-8 就不再调整语言界面。时钟同步、Swap 和 journald 使用仓库中的小脚本:
bash
cd /xqd/infra/mine-linux/server
bash scripts/02-ntp.sh
timedatectl show -p NTPSynchronized --value
bash scripts/06-swap.sh
swapon --show
grep -F '/swapfile-xqd' /etc/fstab
bash scripts/07-journald.sh
systemd-analyze cat-config systemd/journald.conf
journalctl --disk-usage
这台机器只有 2 GiB 内存,因此把全机 Swap 补到 2 GiB。journald 脚本的目标是持久化日志、最多 200 MiB、最长 14 天,并为磁盘预留 1 GiB 空间;实际生效情况还要用上面的命令核对。
3.3 SSH 地址、密钥和主机信任
基础设施域名 infra.xuqiudong.cn 先解析到新云,Jenkins 和小主机都使用这个地址。博客、附件等业务域名继续指向旧云,等迁移完成再切换。
小主机需要把公钥安装到新云:
bash
ssh-copy-id root@infra.xuqiudong.cn
ssh root@infra.xuqiudong.cn true
Jenkins 要在它的实际运行账号下单独验证。私钥始终留在发起连接的一方,目标服务器的 authorized_keys 保存公钥。
首次 SSH 连接还会核对服务器身份,并把结果写进发起方账号的 known_hosts。这和公钥免密是两个环节,后面附件备份遇到的 Host key verification failed 就与此有关。
3.4 UFW、云安全组和 WireGuard
UFW 保留最基本的公网入口,SSH 22 端口始终开放:
bash
ufw insert 1 allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
ufw status numbered
阿里云安全组开放 22/TCP、80/TCP、443/TCP 和 WireGuard 的 51820/UDP。MySQL 3306、Redis 6379、Meilisearch 7700,以及 Java 后端端口 10086、10088 都不向公网开放。
WireGuard 地址为:云主机 10.10.0.1、家中小主机 10.10.0.2、Windows 10.10.0.3。云主机允许 wg0 上来自 10.10.0.0/24 的入站,并允许 Peer 之间经云主机转发;家中小主机允许同一网段从 wg0 入站。
WireGuard 安装并配置好后,云主机的 UFW 规则为:
bash
ufw allow 51820/udp
# 访问云主机本身的服务。
ufw allow in on wg0 from 10.10.0.0/24
# Windows 与小主机等节点之间经云主机转发。
ufw route allow in on wg0 out on wg0 \
from 10.10.0.0/24 to 10.10.0.0/24
Docker 发布端口的流量路径与普通宿主机服务不同,UFW 不能单独覆盖所有 Docker 转发流量。云端用安全组挡住公网入口,家中小主机使用 DOCKER-USER 规则。详细配置见这三篇记录:
4. 安装 Docker 和中间件
4.1 Docker 安装与统一设置
安装脚本采用阿里云 Docker CE 源,依据阿里云安装手册整理,适合国内访问 Docker 官方源不稳定的环境:
bash
cd /xqd/infra/mine-linux/server
bash scripts/10-docker.sh
安装脚本会继续调用 10-docker-config.sh,完成三项设置:Docker Hub 加速地址使用 https://docker.m.daocloud.io;默认日志驱动为 json-file,单文件 10 MiB,保留 3 份;创建外部 bridge 网络 xqd-net。
新服务器没有额外挂载磁盘,因此 Docker 继续使用默认的 /var/lib/docker,没有为了目录形式单独迁移存储位置。
bash
docker compose version
docker info --format '{{.DockerRootDir}}'
docker network inspect xqd-net
cat /etc/docker/daemon.json
docker run --rm hello-world
Docker 日志默认值只应用于新建容器。修改后想让旧容器采用新值,需要重建容器。
软件安装源和镜像加速地址是两回事:前者用于安装 Docker,后者用于拉取 Docker Hub 镜像。加速地址以后不可用时,修改 10-docker-config.sh 开头的配置即可。
4.2 MySQL
MySQL 使用 8.0.46,先加载全局变量,再启动 Compose:
bash
source /etc/profile.d/xqd-global-env.sh
cd /xqd/docker/mysql
docker compose config --quiet
docker compose up -d
docker compose exec mysql mysql -uroot -p
新主库针对 2 GiB 机器做了适度收缩:
server_id=101,与旧主库和家中从库不同。- 开启 binlog,格式为 ROW,只记录两个业务库,保留 30 天。
innodb_buffer_pool_size=128M,max_connections=50。- 关闭
performance_schema和 MySQL X Protocol,降低常驻内存。 innodb_flush_log_at_trx_commit=1、sync_binlog=1,不放宽事务持久化。- 数据挂载在
/xqd/docker/mysql/data,配置文件只读挂载进容器。
sql
SELECT VERSION();
SHOW VARIABLES WHERE Variable_name IN
('innodb_buffer_pool_size','max_connections','performance_schema',
'server_id','log_bin','binlog_format','gtid_mode');
本次目标值为 MySQL 8.0.46、128 MiB、50、OFF、server_id=101、binlog ON、ROW、GTID OFF。root 没有设置为远程账号;业务用户和复制用户在数据导入后创建。
配置见 MySQL Compose 和 my.cnf。注意:
MYSQL_ROOT_PASSWORD只在空数据目录首次初始化时设置密码;已有数据时,修改环境变量不会修改数据库密码。docker compose config --quiet检查配置和变量是否有效,登录查询才是服务验证。- binlog 只包含两个业务库;以后新增数据库时,需要同步调整过滤配置。
- 新云宿主机发布 3306,应用容器始终连接
mysql:3306。
4.3 Redis
bash
source /etc/profile.d/xqd-global-env.sh
cd /xqd/docker/redis
docker compose config --quiet
docker compose up -d
docker compose exec redis sh -c \
'REDISCLI_AUTH="$REDIS_PASSWORD" redis-cli ping'
返回 PONG 表示服务和认证正常。Redis 使用 7.4,开启 AOF,每秒刷盘,不另外做定时 RDB 快照;旧 Redis 数据不迁移。
部署文件为 Redis Compose 和 redis.conf。
容器内端口一直是 6379。博客和附件使用 redis:6379;小主机需要访问时,使用 WireGuard 地址 10.10.0.1:6379。主机发布端口由 REDIS_HOST_PORT 决定,云安全组不开放这个端口。
4.4 Meilisearch
Meilisearch 固定为 v1.54.3,Java SDK 仍使用 0.20.1。本地演练时已经验证建索引和查询可以正常工作。
bash
source /etc/profile.d/xqd-global-env.sh
cd /xqd/docker/meilisearch
docker compose config --quiet
docker compose up -d
curl http://127.0.0.1:7700/health
docker stats --no-stream meilisearch
生产模式设置至少 16 字节的 Master Key。索引任务最多使用 128 MiB、1 个线程;容器上限为 512 MiB 内存、1 个 CPU;数据挂载到 /xqd/docker/meilisearch/data,匿名统计关闭,日志级别为 WARN。重建索引时观察实际内存,容器发生 OOM 再调整限额。
部署文件为 Meilisearch Compose 和 meilisearch.toml。
同机博客容器使用 http://meilisearch:7700;跨主机管理使用 http://10.10.0.1:7700。旧索引不复制,应用发布后重新初始化。
5. 安装 Nginx、证书和日志策略
5.1 为什么使用 Nginx 官方仓库
旧云由 Ubuntu apt 安装 Nginx 1.18。新配置中的家庭服务 upstream 使用 resolve 跟随 DDNS 地址变化,这项写法要求开源 Nginx 版本不低于 1.27.3,因此新云改用 Nginx 官方 stable 仓库。
bash
apt-get update
apt-get install -y curl ca-certificates gnupg ubuntu-keyring \
openssl cron logrotate
curl -fsSL https://nginx.org/keys/nginx_signing.key |
gpg --dearmor --batch --yes \
-o /usr/share/keyrings/nginx-archive-keyring.gpg
source /etc/os-release
echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] https://nginx.org/packages/ubuntu $VERSION_CODENAME nginx" \
> /etc/apt/sources.list.d/nginx.list
apt-get update
apt-get install -y nginx
nginx -v
云端博客和附件分别代理到宿主机的 127.0.0.1:10086、127.0.0.1:10088。task、wiki、test、duoduo 等家庭服务通过 xuqiudong.us.to 转发。
Nginx 的 DNS 结果缓存 5 分钟,upstream 的 resolve 负责跟随域名地址变化,不检查家中小主机是否在线。家中关机时,对应请求返回 503 页面;不再用脚本每半小时检查域名并反复重启 Nginx。
5.2 使用 acme.sh 签发泛域名证书
acme.sh 继续安装在云主机,因为云主机是主要的证书使用方。泛域名证书使用阿里云 DNS 验证,不要求家中开放 80 或 443。
bash
curl -fsSL https://get.acme.sh | sh
source /etc/profile.d/xqd-global-env.sh
/root/.acme.sh/acme.sh --issue --server letsencrypt --dns dns_ali \
-d "$DOMAIN" -d "*.$DOMAIN" --keylength 2048
mkdir -p /etc/nginx/pem
/root/.acme.sh/acme.sh --install-cert -d "$DOMAIN" \
--key-file /etc/nginx/pem/key.pem \
--fullchain-file /etc/nginx/pem/full.pem \
--reloadcmd 'nginx -t && systemctl reload nginx'
chmod 600 /etc/nginx/pem/key.pem
--install-cert 很关键。Nginx 使用固定的 /etc/nginx/pem 路径,acme.sh 续期后把新证书复制到这里,再执行 reload hook。Nginx 配置不直接引用 acme.sh 内部目录。下载受限时可参考 acme.sh 国内安装说明。
5.3 安装站点配置
bash
cd /xqd/infra/mine-linux/server
mkdir -p /etc/nginx/xqd
cp -a config/nginx/conf.d config/nginx/snippets \
config/nginx/html /etc/nginx/xqd/
cp config/nginx/nginx.conf /etc/nginx/nginx.conf
nginx -t
systemctl enable --now nginx
systemctl reload nginx
apt 安装后 Nginx 可能已经启动;复制新配置后要执行 reload,enable --now 不会让已运行的进程重新加载配置。
配置安装后的目录为:
text
/etc/nginx/
├── nginx.conf TLS、DNS 解析、日志及公共设置
├── pem/ full.pem、key.pem
└── xqd/
├── conf.d/ 各站点与家庭 upstream
├── snippets/ 公共代理参数、503 错误处理
└── html/ 服务不可用提示页
主配置只加载 /etc/nginx/xqd/conf.d/*.conf。这样不会把旧的默认站点、临时文件或遗留配置一起加载。各站点只保留域名、后端、日志和必要差异,重复的 TLS 与代理设置抽到公共配置中。
此时可以检查证书和 Nginx 配置;博客等应用尚未发布,后端访问验证留到第 9 节。
bash
/root/.acme.sh/acme.sh --list
openssl x509 -in /etc/nginx/pem/full.pem -noout \
-issuer -dates -ext subjectAltName
nginx -t
5.4 三种日志分别限制
系统日志交给 journald;容器标准输出交给 Docker 的日志驱动;Nginx 文件日志单独配置 logrotate。它们不会共用一套保留策略。
bash
cd /xqd/infra/mine-linux/server
cp config/nginx/logrotate /etc/logrotate.d/nginx
chmod 644 /etc/logrotate.d/nginx
systemctl enable --now logrotate.timer
# 只检查规则,不执行轮转。
logrotate --debug /etc/logrotate.conf
- access 日志每天轮转,保留 7 份,超过 50 MiB 时可提前轮转。
- error 日志每月轮转,保留 3 份,超过 20 MiB 时可提前轮转。
- 文件大小在 logrotate 被调度时检查,不是写到阈值就立即截断。
- 轮转后向 Nginx 发送
USR1重新打开日志文件,不重启服务。
5.5 保留必要的定时任务
确认 cron 已运行,查看 root 当前任务,acme.sh 安装器生成的续期任务不重复添加:
bash
systemctl enable --now cron
crontab -l
crontab -e
cron
# acme.sh 检查是否需要续期,成功续期后执行已保存的 reload hook。
28 0 * * * /root/.acme.sh/acme.sh --cron --home /root/.acme.sh > /dev/null
# 保留每日平滑重载,先检查配置。
15 1 * * * /usr/sbin/nginx -t && /usr/bin/systemctl reload nginx
acme.sh 的 DNS 插件会保存后续续期所需的凭证,因此不能指望 cron 自动读取 /etc/profile.d。首次签发和续期任务要分别验证。本次两项都已经测试。
旧的 wiki-check.sh 不再迁移。家庭小主机关机时,周期性检查脚本没有必要反复重启 Nginx。
6. 在停写之前预同步文件
6.1 新云登录旧云
新云需要主动从旧云拉取附件和静态文件,因此在新云生成独立密钥,并把公钥安装到旧云:
bash
# 已有可用密钥时跳过生成,不覆盖。
ssh-keygen -t ed25519
ssh-copy-id -i /root/.ssh/id_ed25519.pub root@旧云公网IP
ssh -i /root/.ssh/id_ed25519 root@旧云公网IP true
id_ed25519 是私钥,只留在新云;id_ed25519.pub 是安装到旧云的公钥。首次连接核对并接受旧云主机指纹。
这里使用旧云固定 IP,不使用即将切换的业务域名,否则 DNS 改完后可能从新云连接到新云自身。
6.2 第一次同步
bash
apt-get install -y rsync
OLD_SERVER_HOST='填写旧云公网IP'
mkdir -p /xqd/attachments \
/xqd/server/nav \
/xqd/server/duoduo-school-district
rsync -a "root@$OLD_SERVER_HOST:/xqd/attachments/" \
/xqd/attachments/
rsync -a "root@$OLD_SERVER_HOST:/xqd/server/nav/" \
/xqd/server/nav/
rsync -a "root@$OLD_SERVER_HOST:/xqd/server/duoduo-school-district/" \
/xqd/server/duoduo-school-district/
-a 保留权限、修改时间等元数据;源路径末尾的 / 表示复制目录内容。准备阶段不加 --delete,避免删除新云目标目录中已有的文件。
第一次同步完成后,旧站继续运行。切换时再次执行相同命令,大小和修改时间等信息一致的文件会跳过。
7. 切换 MySQL 主库
7.1 当前条件
迁移前,旧云 MySQL 8.0.35 是主库,家中 MySQL 8.0.46 是从库。两个节点的 gtid_mode 都是 OFF,使用 binlog 文件名和位置复制;家中从库数据完整,并且每天另做 dump。
新云使用 MySQL 8.0.46,已经开启 binlog。迁移方法是让家中从库先追平旧云,从家中导出一致数据;新云导入后记录自己的 binlog 坐标,再让家中 MySQL 改为跟随新云。
7.2 停止业务写入并等待追平
先暂停小主机 02:00 的附件同步任务,再停止旧云和小主机中会写业务库的应用、定时任务。旧云 MySQL 保持运行,以便从库拉取最后的 binlog。
旧云记录最终坐标:
sql
-- 记录 File 和 Position,待从库执行到同一位置。
SHOW MASTER STATUS;
家中从库检查复制:
sql
SHOW REPLICA STATUS\G
需要同时确认:
Replica_IO_Running和Replica_SQL_Running都是Yes。- 没有复制错误。
Relay_Source_Log_File、Exec_Source_Log_Pos已到旧云最终坐标。
Seconds_Behind_Source=0 可以辅助判断,但不单独作为追平依据。确认后在家中停止复制,保留已有数据和连接配置:
sql
STOP REPLICA;
MySQL 主从是从库主动连接主库拉取 binlog。新主库有自己的 binlog 文件和位置,仅修改域名解析不会自动完成主库切换。
7.3 从家中从库导出
家中小主机执行:
bash
# dump 中包含业务数据,限制新文件权限。
umask 077
mysqldump -uroot -p --single-transaction --routines --events --triggers \
--hex-blob --set-gtid-purged=OFF --no-tablespaces \
--databases lcxm_backend lcxm_attachment \
> /tmp/xqd-final.sql
scp /tmp/xqd-final.sql \
root@infra.xuqiudong.cn:/tmp/xqd-final.sql
参数含义:
--single-transaction:获得 InnoDB 一致快照;导出期间不要改表结构。--databases:导出建库语句、表结构和数据。--routines --events --triggers:保留存储过程、事件和触发器。本次两个库没有事件。--hex-blob:二进制字段使用十六进制表示。--set-gtid-purged=OFF:不写入 GTID_PURGED 设置。--no-tablespaces:不导出表空间定义。
这个 dump 不包含 MySQL 用户和权限,账号要在新云单独创建。
7.4 导入新云并创建账号
新云执行:
bash
docker exec -i mysql sh -c \
'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" exec mysql -uroot' \
< /tmp/xqd-final.sql
docker exec -it mysql mysql -uroot -p
-i 让容器内 mysql 客户端从宿主机标准输入读取 dump。这里使用容器初始化时的 root 密码;如果之后改过数据库密码,需使用实际密码登录。
原来的 qiudong 拥有过多全局管理权限。这次只给两个业务库授权,并单独建立复制账号,以下密码和复制账号名按实际情况填写:
sql
CREATE USER 'qiudong'@'%' IDENTIFIED BY '填写应用密码';
GRANT ALL PRIVILEGES ON lcxm_backend.* TO 'qiudong'@'%';
GRANT ALL PRIVILEGES ON lcxm_attachment.* TO 'qiudong'@'%';
SHOW GRANTS FOR 'qiudong'@'%';
CREATE USER 'xqd_repl'@'%' IDENTIFIED BY '填写复制密码';
GRANT REPLICATION SLAVE ON *.* TO 'xqd_repl'@'%';
-- 导入和账号准备完成后,记录新主库自己的 File、Position。
SHOW MASTER STATUS;
'%' 是 MySQL 账号的来源匹配,不代表公网应开放 3306。家中从库通过 WireGuard 访问,公网仍由阿里云安全组隔离。
从导出开始,到新云导入并接好从库之前,业务继续停写。新云和家中从库应保持相同的业务数据,后续才从新主库坐标开始接收新增变更。
7.5 家中 MySQL 改为跟随新云
先从小主机验证 10.10.0.1:3306 可达,再在家中 MySQL执行:
sql
CHANGE REPLICATION SOURCE TO
SOURCE_HOST='10.10.0.1',
SOURCE_PORT=3306,
SOURCE_USER='xqd_repl',
SOURCE_PASSWORD='填写复制密码',
SOURCE_AUTO_POSITION=0,
SOURCE_LOG_FILE='填写新主库binlog文件',
SOURCE_LOG_POS=填写新主库位置,
GET_SOURCE_PUBLIC_KEY=1;
START REPLICA;
SHOW REPLICA STATUS\G
SOURCE_AUTO_POSITION=0表示继续使用文件和位置,不启用 GTID 自动定位。SOURCE_LOG_POS填上一步记录的整数,不加引号。GET_SOURCE_PUBLIC_KEY=1允许获取密码认证所需的服务器 RSA 公钥;传输路径仍是 WireGuard。- 小主机已经拥有导出时的完整数据,不需要重新导入。
- 不执行
RESET MASTER或RESET REPLICA ALL。
确认 IO、SQL 线程都为 Yes,没有错误,再在新主库做一次可核对的修改。本次修改已经成功同步到小主机。
7.6 为什么导入后表空间变小
导入后,DBeaver 显示部分表的占用空间比旧库小。我抽查了几个表,行数一致。
mysqldump 是逻辑迁移。导入时会重建表和索引,原数据文件中的空闲空间不会原样复制;客户端显示的表大小和行数还可能来自统计信息。因此,空间变小本身不能说明数据丢失。
核对时应查询实际行数、关键记录,并验证应用查询。本次确认的是抽查表行数一致,没有做全库逐行校验。
8. 最终同步文件并发布应用
8.1 补齐最后的附件变化
旧附件服务停止写入后,在新云再次运行第 6 节的三条 rsync 命令。然后做一次只读检查:
bash
# 如果换过 shell,先重新设置为旧云的固定 IP。
OLD_SERVER_HOST='填写旧云公网IP'
rsync -a --dry-run --itemize-changes \
"root@$OLD_SERVER_HOST:/xqd/attachments/" \
/xqd/attachments/
这次命令没有输出,表示按 rsync 默认比较规则没有发现需要同步的变化。它不是逐文件内容校验,也不会报告只存在于目标端的多余文件,因为命令没有使用 --checksum 和 --delete。
8.2 Java 容器改用 Docker 服务名
博客和附件都加入 xqd-net。博客使用 application-docker.properties,附件使用 application-prod.properties,中间件连接改为:
properties
ph.mysql.host=mysql
ph.mysql.port=3306
ph.redis.host=redis
ph.redis.port=6379
ph.meilisearch.host=http://meilisearch:7700/
MySQL、Redis 配置用于两个应用,Meilisearch 只用于博客。账号、密码和 API key 仍写入各应用自己的生产配置,不在 docker run 中重复维护;浏览器访问的附件地址保持为 https://attachment.xuqiudong.cn。
服务名只在共同 Docker 网络中可用。小主机上的 console 等跨主机程序需要改为云端 WireGuard 地址和对应宿主机端口。
原来的 --add-host=host.docker.internal:host-gateway 可以保留,它与 xqd-net 不冲突;这三个中间件已经不再通过宿主机地址连接。Meilisearch 也不再经过旧 Nginx 84 端口。
如果旧环境中的容器仍要连接宿主机服务,加入新网桥会改变容器来源网段,必须同时核对宿主机服务监听地址和 UFW 放行规则。不能认为只是增加一个 --network 就完全没有访问影响。
8.3 Jenkins 镜像传输增加 gzip
Java 应用继续沿用 Jenkinsfile 中的 docker run。发布地址统一改为 infra.xuqiudong.cn,容器启动时加入 --network xqd-net。
镜像传输调整为:
text
docker save
→ gzip
→ scp 镜像.tar.gz
→ 新云解压并覆盖原 tar
→ docker load
→ 更新容器
我测试的镜像包压缩后约为原来的 50%。原来一次 SCP 要 4 到 5 分钟,因此保留了压缩。远端完成解压和 docker load 后,再更新应用容器;传输或解压失败时不先停止运行容器。
博客和附件两条流水线都已经执行成功。附件发布时选择 DEPLOY_CLOUD=true、DEPLOY_LOCAL=false,避免同时重建家中测试容器。
8.4 重建搜索索引
Meilisearch 不搬旧索引。博客启动后执行应用已有的初始化流程,再验证查询。本次初始化成功,页面可以正常搜索。
9. 调整备份来源并切换 DNS
9.1 小主机附件备份改到新云
小主机每天 02:00 拉取云端附件,目标目录不变,只修改远程来源:
ini
root@infra.xuqiudong.cn:/xqd/attachments/prod=/xqd/backup/lcxm-attachment/prod
首次运行时报过:
text
Host key verification failed.
公钥已经配置好,问题出在 known_hosts。在实际执行定时任务的账号下,先手动 SSH,核对并接受新云主机指纹:
bash
ssh root@infra.xuqiudong.cn true
/xqd/mine-shell/backup/attachment-remote-backup.sh
tail -n 30 \
/xqd/logs/mine-shell/backup/attachment-remote-backup.log
附件备份脚本使用 rsync --delete,目标是云端附件的镜像。它会先通过 SSH 检查远程目录是否存在;连接失败或目录不存在时不启动 rsync。新云数据确认完整后,才恢复 02:00 的任务。
小主机 01:00 的本地目录备份和每日 MySQL dump 不需要修改。
9.2 先用新云验证站点
业务 DNS 还指向旧云时,可以只让一条测试请求走新 IP:
bash
nginx -t
NEW_SERVER_IP='填写新云公网IP'
# 保留域名对应的 Host 和 TLS SNI,只替换本次请求的目标地址。
curl -I --resolve "xuqiudong.cn:443:$NEW_SERVER_IP" \
https://xuqiudong.cn/
curl -I --resolve "wiki.xuqiudong.cn:443:$NEW_SERVER_IP" \
https://wiki.xuqiudong.cn/
逐项检查:
- 博客页面、登录和主要接口。
- 附件地址、已有文件和新上传。
- Redis 连接、Meilisearch 搜索。
- nav、task、wiki 等站点,特别是家庭服务代理。
- 新云写入后,家中从库能否同步。
curl -I 只验证 HTTP 响应,不能代替这些业务检查。写入测试限定在新主库,旧云仍保持停写。
9.3 切换业务 DNS
确认数据库复制、附件最终同步和应用发布完成后,在阿里云 DNS 中把业务域名解析到新服务器,再恢复新云业务写入。旧云应用保持停止,避免 DNS 缓存期间两边同时接收写入。
新主库一旦产生新的业务数据,回退就不能只改 DNS。必须先处理新云产生的增量数据,再决定如何恢复旧主库。
10. 验证结果与收尾
10.1 已确认
- MySQL 两个业务库已经导入,抽查表行数一致。
- 新主库的修改可以同步到家中从库。
- 博客和附件两条 Jenkins 流水线发布成功。
- 旧附件能够查看,附件最终 rsync 检查无待传变化。
- Meilisearch 索引初始化成功,可以搜索。
- nav、task、wiki 等站点访问正常。
- WireGuard 三个节点可以互通。
- acme.sh 可以签发证书,自动续期任务已经测试。
- 小主机附件备份来源已经改为
infra.xuqiudong.cn。 - 业务 DNS 已经切换到新云。
- 2026 年 11 月 6 日 15:00 收到管局短信,备案信息变更完成。
10.2 收尾清单
- 实际上传一个新附件,验证写入、下载和后续备份。
- 检查日志实际轮转后的文件与权限。
- 核对 journald 实际配置及日志占用。
- 择机重启整台新云,检查 WireGuard、Docker 中间件、Java 应用和 Nginx 是否恢复。
- 清理
/tmp/xqd-final.sql等临时文件。 - 观察稳定后确认旧云应用停用并下线旧机。
- 在迁移记录中补充实际 DNS 切换时间。
三个中间件的 Compose 使用 restart: unless-stopped;Java 应用沿用 Jenkinsfile 的重启策略。容器当前运行正常不能替代整机重启验证,这一项需要实际操作后再勾选。
11. 下次迁移如何复用
这次保留了已经跑通的 Jenkins 发布方式,把中间件安装和持久化配置交给 Compose,Nginx 继续在宿主机运行。下次迁移时,不需要重新找安装命令和猜测配置文件在哪里。
mine-linux 仓库保存以下资源:
server/:0–19 执行清单、系统脚本、Compose、Nginx 和全局变量样例。home/:小主机备份脚本、来源配置和定时任务说明。docs/migrations/:迁移前环境、实际结果和收尾记录。docs/blog/:本次迁移文章和网络防护系列。
脚本、配置和完整执行清单保存在 Gitee 仓库 xuqiudong/mine-linux。仓库只提交样例值;服务器密码、阿里云密钥、证书私钥、数据库文件和附件不入库。
下次仍要根据实际环境填写服务器地址和凭证,核对 MySQL 最终坐标、附件差异和 DNS。安装脚本、目录、服务配置、备份任务与检查顺序则可以沿用,修改后再逐项验证。
GitHub 登录
Gitee 登录
QQ 登录
请围绕文章内容交流,勿发布广告、违法信息或他人隐私。