博客: 临窗旋墨
2026 年 10 月 30 日,我购买了一台XX云服务器,配置为 2 核、2 GiB 内存、3 Mbps 带宽,系统使用 Ubuntu 24.04。旧云服务器还有一个月到期,这次迁移要把网站、中间件、附件和相关任务完整搬过去,同时保留家中小主机上的 MySQL 从库和附件备份。
我的个人网站可以接受短暂停写,因此没有设计双写或自动故障转移。迁移顺序定为:旧站继续运行时准备新环境并预同步文件;切换当天暂停写入,迁移 MySQL 和最后一批文件;应用验证通过后切换 DNS。
目前业务已经切到新云。MySQL 主从同步、旧附件访问、搜索和主要站点已经验证;新附件上传、日志实际轮转和整机重启恢复仍列在收尾清单中。
旧云运行 Ubuntu 20.04,配置同样是 2 核、2 GiB 内存。博客和附件服务由家中小主机的 Jenkins 构建镜像,再通过 SCP 传到云服务器,最后用 docker run 发布。
旧环境的中间件比较分散:MySQL、Redis 和 Meilisearch 直接安装在宿主机,Nginx 也在宿主机;家中小主机是 MySQL 从库,每天还会拉取云端附件。Java 应用在 Docker 中访问宿主机中间件,使用过 host.docker.internal,Meilisearch 甚至经过 Nginx 的 84 端口代理。
这套环境一直能用,但下次迁移仍要重新安装、重新找配置。于是这次顺手做了三项整理:
docker run,不在迁移期间同时改发布方式。mine-linux 仓库。
图中主从同步、附件备份的箭头表示数据流向,连接由小主机主动发起。
lcxm_backend、lcxm_attachment 两个业务库,并把家中从库接到新主库。准备阶段不影响旧站:
切换阶段才暂停写入:
数据库安装和数据迁移特意分开。MySQL 容器可以提前启动和验证;真正的数据导入留在停写窗口中完成。
以下命令以 Ubuntu 24.04、root 账号为例。项目保存在 /xqd/infra/mine-linux/,不采用这个路径时,相应修改命令中的项目位置。
服务器上的资源统一放在 /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_)'时区、字符集和主机名先处理:
bash复制代码
timedatectl set-timezone Asia/Shanghai
timedatectl
locale charmap
hostnamectl set-hostname "$NEW_HOSTNAME"
hostnamectl --staticlocale 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 空间;实际生效情况还要用上面的命令核对。
基础设施域名 infra.xuqiudong.cn 先解析到新云,Jenkins 和小主机都使用这个地址。博客、附件等业务域名继续指向旧云,等迁移完成再切换。
小主机需要把公钥安装到新云:
bash复制代码
ssh-copy-id root@infra.xuqiudong.cn
ssh root@infra.xuqiudong.cn trueJenkins 要在它的实际运行账号下单独验证。私钥始终留在发起连接的一方,目标服务器的 authorized_keys 保存公钥。
首次 SSH 连接还会核对服务器身份,并把结果写进发起方账号的 known_hosts。这和公钥免密是两个环节,后面附件备份遇到的 Host key verification failed 就与此有关。
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/24Docker 发布端口的流量路径与普通宿主机服务不同,UFW 不能单独覆盖所有 Docker 转发流量。云端用安全组挡住公网入口,家中小主机使用 DOCKER-USER 规则。详细配置见这三篇记录:
安装脚本采用云 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-worldDocker 日志默认值只应用于新建容器。修改后想让旧容器采用新值,需要重建容器。
软件安装源和镜像加速地址是两回事:前者用于安装 Docker,后者用于拉取 Docker Hub 镜像。加速地址以后不可用时,修改 10-docker-config.sh 开头的配置即可。
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,与旧主库和家中从库不同。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 检查配置和变量是否有效,登录查询才是服务验证。mysql:3306。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 决定,云安全组不开放这个端口。
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。旧索引不复制,应用发布后重新初始化。
旧云由 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。
acme.sh 继续安装在云主机,因为云主机是主要的证书使用方。泛域名证书使用XX云 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 国内安装说明。
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 nginxapt 安装后 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系统日志交给 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.confUSR1 重新打开日志文件,不重启服务。确认 cron 已运行,查看 root 当前任务,acme.sh 安装器生成的续期任务不重复添加:
bash复制代码
systemctl enable --now cron
crontab -l
crontab -ecron复制代码
# 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 nginxacme.sh 的 DNS 插件会保存后续续期所需的凭证,因此不能指望 cron 自动读取 /etc/profile.d。首次签发和续期任务要分别验证。本次两项都已经测试。
旧的 wiki-check.sh 不再迁移。家庭小主机关机时,周期性检查脚本没有必要反复重启 Nginx。
新云需要主动从旧云拉取附件和静态文件,因此在新云生成独立密钥,并把公钥安装到旧云:
bash复制代码
# 已有可用密钥时跳过生成,不覆盖。
ssh-keygen -t ed25519
ssh-copy-id -i /root/.ssh/id_ed25519.pub root@旧云公网IP
ssh -i /root/.ssh/id_ed25519 root@旧云公网IP trueid_ed25519 是私钥,只留在新云;id_ed25519.pub 是安装到旧云的公钥。首次连接核对并接受旧云主机指纹。
这里使用旧云固定 IP,不使用即将切换的业务域名,否则 DNS 改完后可能从新云连接到新云自身。
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,避免删除新云目标目录中已有的文件。
第一次同步完成后,旧站继续运行。切换时再次执行相同命令,大小和修改时间等信息一致的文件会跳过。
迁移前,旧云 MySQL 8.0.35 是主库,家中 MySQL 8.0.46 是从库。两个节点的 gtid_mode 都是 OFF,使用 binlog 文件名和位置复制;家中从库数据完整,并且每天另做 dump。
新云使用 MySQL 8.0.46,已经开启 binlog。迁移方法是让家中从库先追平旧云,从家中导出一致数据;新云导入后记录自己的 binlog 坐标,再让家中 MySQL 改为跟随新云。
先暂停小主机 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 文件和位置,仅修改域名解析不会自动完成主库切换。
家中小主机执行:
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 用户和权限,账号要在新云单独创建。
新云执行:
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 访问,公网仍由XX云安全组隔离。
从导出开始,到新云导入并接好从库之前,业务继续停写。新云和家中从库应保持相同的业务数据,后续才从新主库坐标开始接收新增变更。
先从小主机验证 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\GSOURCE_AUTO_POSITION=0 表示继续使用文件和位置,不启用 GTID 自动定位。SOURCE_LOG_POS 填上一步记录的整数,不加引号。GET_SOURCE_PUBLIC_KEY=1 允许获取密码认证所需的服务器 RSA 公钥;传输路径仍是 WireGuard。RESET MASTER 或 RESET REPLICA ALL。确认 IO、SQL 线程都为 Yes,没有错误,再在新主库做一次可核对的修改。本次修改已经成功同步到小主机。
导入后,DBeaver 显示部分表的占用空间比旧库小。我抽查了几个表,行数一致。
mysqldump 是逻辑迁移。导入时会重建表和索引,原数据文件中的空闲空间不会原样复制;客户端显示的表大小和行数还可能来自统计信息。因此,空间变小本身不能说明数据丢失。
核对时应查询实际行数、关键记录,并验证应用查询。本次确认的是抽查表行数一致,没有做全库逐行校验。
旧附件服务停止写入后,在新云再次运行第 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。
博客和附件都加入 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 就完全没有访问影响。
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,避免同时重建家中测试容器。
Meilisearch 不搬旧索引。博客启动后执行应用已有的初始化流程,再验证查询。本次初始化成功,页面可以正常搜索。
小主机每天 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 不需要修改。
业务 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/逐项检查:
curl -I 只验证 HTTP 响应,不能代替这些业务检查。写入测试限定在新主库,旧云仍保持停写。
确认数据库复制、附件最终同步和应用发布完成后,在XX云 DNS 中把业务域名解析到新服务器,再恢复新云业务写入。旧云应用保持停止,避免 DNS 缓存期间两边同时接收写入。
新主库一旦产生新的业务数据,回退就不能只改 DNS。必须先处理新云产生的增量数据,再决定如何恢复旧主库。
infra.xuqiudong.cn。/tmp/xqd-final.sql 等临时文件。三个中间件的 Compose 使用 restart: unless-stopped;Java 应用沿用 Jenkinsfile 的重启策略。容器当前运行正常不能替代整机重启验证,这一项需要实际操作后再勾选。
这次保留了已经跑通的 Jenkins 发布方式,把中间件安装和持久化配置交给 Compose,Nginx 继续在宿主机运行。下次迁移时,不需要重新找安装命令和猜测配置文件在哪里。
mine-linux 仓库保存以下资源:
server/:0–19 执行清单、系统脚本、Compose、Nginx 和全局变量样例。home/:小主机备份脚本、来源配置和定时任务说明。docs/migrations/:迁移前环境、实际结果和收尾记录。docs/blog/:本次迁移文章和网络防护系列。脚本、配置和完整执行清单保存在 Gitee 仓库 xuqiudong/mine-linux。仓库只提交样例值;服务器密码、XX云密钥、证书私钥、数据库文件和附件不入库。
下次仍要根据实际环境填写服务器地址和凭证,核对 MySQL 最终坐标、附件差异和 DNS。安装脚本、目录、服务配置、备份任务与检查顺序则可以沿用,修改后再逐项验证。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。