Docker 游戏服务器看起来很好迁移:保存镜像、复制配置和存档,再到另一台机器上重新启动容器即可。

真正实践之后会发现,事情远没有这么简单。

一个真正合格的迁移包,需要回答的并不是:

文件有没有备份?

而是:

如果原服务器彻底不存在,只剩下这个备份包,能不能在另一台 Linux + Docker 主机上重新启动服务器,并继续读取原来的世界和存档?

这两者之间存在很大差别。

本文以 Don’t Starve Together(DST)Palworld 两套 Docker 游戏服务器为例,介绍一套相对可靠的 Docker 游戏服务器迁移方法。

所有涉及实际服务器路径、用户名、端口、域名、镜像摘要、世界 ID、时间戳等信息均已脱敏。


一、首先明确什么叫“可迁移备份”

普通备份可能只包含:

docker-compose.yml
配置文件
游戏存档

这样的备份当然有价值,但还不能证明它可以恢复。

完整迁移至少涉及四个部分:

Docker image
+
容器运行配置
+
持久化数据
+
恢复说明

进一步说,一个理想的迁移包应该具备:

migration-package/
├── docker/
│   └── images/
├── config/
├── data/
├── metadata/
├── README-MIGRATION.md
└── MANIFEST.sha256

其中最关键的是:

不能依赖原服务器上仍然存在的东西。

例如,如果恢复过程中仍然需要:

docker pull some-image:latest

那么这并不是严格意义上的自包含迁移。

因为几年之后:

  • 镜像可能被删除;
  • latest 可能已经指向另一个版本;
  • Registry 可能不存在;
  • 上游镜像可能发生不兼容变化。

因此,Docker image 本身也应该成为备份的一部分。


二、先搞清楚数据到底在哪里

Docker 应用的数据并不一定存储在 Docker volume 中。

实际部署中常见三种情况。

1. Named Volume

例如:

volumes:
  - game-data:/data

这种情况下,需要单独备份 Docker volume。


2. Bind Mount

例如:

volumes:
  - ./data:/data
  - ./mods:/mods
  - ./logs:/logs

这时真正的数据其实就在宿主机目录里。

DST 案例正属于这种情况。

其主要内容包括:

data
runtime
mods
logs

因此迁移重点不是 Docker volume,而是整个应用目录的完整冷备份。


3. 容器 Writable Layer

这是最容易被忽略的一层。

即使 Compose 中已经配置了 volume 或 bind mount,仍然不能直接假设:

所有重要数据一定都在挂载目录里。

如果过去曾经:

docker exec

进入容器并手工修改过文件,那么这些修改可能只存在于容器 writable layer 中。

因此正式迁移前最好检查:

docker diff <container>

如果容器已经不存在,则至少需要确认历史运行方式没有依赖容器内部的临时修改。


三、备份 Docker image,而不是只记录镜像名称

例如某个 Compose 文件写着:

image: vendor/game-server:latest

这并不意味着未来执行:

docker pull vendor/game-server:latest

就能得到现在正在使用的版本。

latest 只是标签,不是不可变版本。

实际迁移中应该记录:

Image ID
RepoDigest
Architecture
OS
Entrypoint
Cmd
Environment
WorkingDir
User
Volumes
ExposedPorts
Healthcheck

然后导出镜像:

docker image save \
  -o game-image.tar \
  vendor/game-server:latest

最终恢复时:

docker load -i game-image.tar

这样即使上游镜像已经发生变化,仍然能够重新得到当时使用的镜像。


四、docker save 并不一定马上开始写文件

这次 DST 迁移过程中出现了一个比较有意思的现象。

执行:

docker image save -o game-image.tar <image>

之后,很长一段时间看不到正式的:

game-image.tar

反而出现类似:

.tmp-game-image.tarXXXXXXXX

这样的临时文件。

开始时它甚至可能是:

0 bytes

与此同时磁盘却持续进行大量 I/O。

这并不一定意味着命令已经失败。

Docker CLI 在使用 -o 输出时,可能先创建临时文件,随后等待 Docker daemon 准备 image export。

内部可能需要:

解析 image metadata
读取 layer
遍历 content store
准备 tar 数据流

只有 daemon 真正开始向客户端发送数据之后,临时文件才开始增长。

因此实际观察到的过程可能是:

.tmp file = 0 B
        ↓
磁盘持续大量读取
        ↓
等待数分钟
        ↓
.tmp file 开始快速增加
        ↓
数 GB
        ↓
导出完成
        ↓
.tmp 原子重命名为正式 .tar

判断是否真正卡死时,不应该只依赖硬盘声音。

更好的观察方式是:

watch -n 2 'ls -lh .tmp-game-image*'

如果文件大小开始增长,说明镜像数据流已经正常输出。


五、docker savedocker export 完全不是一回事

这是 Docker 迁移中非常重要的区别。

docker save

针对:

image

保存:

layer
image metadata
tag
manifest
config

恢复使用:

docker load

docker export

针对:

container filesystem

得到的是一个 root filesystem:

docker export <container>

它不包含完整 Docker image metadata。

因此不能直接:

docker load

恢复。

两者关系大致是:

docker save
    ↓
完整 Docker image archive
    ↓
docker load


docker export
    ↓
root filesystem tar
    ↓
需要 docker import / 重建配置

因此如果目标是完整迁移,优先使用 docker save

docker export 更适合作为灾难恢复的备用资产,而不是主要恢复方案。


六、为什么只打包还不够

即使已经拥有:

Docker image
Compose
配置
世界存档
mods

仍然不能直接宣布:

Backup: PASS

因为文件存在,并不代表它们组合起来能够工作。

可靠的迁移必须进行一次:

独立恢复测试。

关键原则是:

测试不能使用原始正式目录。

假设正式数据位于:

/srv/game

恢复测试不能直接:

volumes:
  - /srv/game:/data

否则即使容器启动成功,也只能证明原服务器上的数据仍然可用。

正确方法应该是:

正式目录
    ↓
cold copy
    ↓
migration staging
    ↓
测试恢复目录
    ↓
临时容器

恢复测试必须只读取迁移包中的副本。


七、DST:Master 和 Caves 必须同时验证

DST 的 Dedicated Server 通常不是一个单独实例。

典型结构包括:

Master
Caves

因此只看到:

Master container: running

并不足以认为恢复成功。

至少需要验证:

Master 正常启动
Caves 正常启动
Master / Caves 都使用预期镜像
cluster 被识别
原世界被识别
save 被识别
modoverrides.lua 被读取
Workshop mods 被加载

还必须特别关注:

有没有生成一个新的世界。

如果服务器因为找不到原来的存档而自动初始化了新世界,Docker 本身仍然可能显示:

running

但迁移实际上已经失败。

因此日志验证非常重要。

真正有效的证据通常是:

读取到了以前存在的 world/session
识别到了已有 save version
识别到了现有 mods
没有出现 fresh-world initialization

这才说明:

迁移后的服务器真的接着原来的世界继续运行。


八、Palworld:存档识别比“容器启动成功”更重要

Palworld 同样如此。

成功恢复至少要确认:

服务器进程正常启动
原 SaveGames 目录存在
Level.sav 被识别
LevelMeta.sav 被识别
Config 被读取
没有重新初始化新世界

可以进一步对关键存档做 SHA-256:

sha256sum Level.sav

然后对比:

正式数据 hash
最终备份 hash

如果一致,就能够进一步证明最终归档中的存档与原始服务器一致。


九、为什么恢复测试后还要重新做一次 Cold Copy

游戏服务器在启动过程中通常会修改:

save
logs
runtime
metadata

因此,如果直接拿恢复测试使用过的数据作为最终备份,就可能把测试行为写进迁移包。

更稳妥的流程是:

原正式数据
    ↓
第一次 cold copy
    ↓
恢复测试
    ↓
验证 PASS
    ↓
删除测试数据
    ↓
再次从正式目录 cold copy
    ↓
制作最终迁移包

这样最终归档保存的仍然是正式服务器停止时的原始状态。


十、迁移包必须有内部完整性清单

最终目录应该生成:

MANIFEST.sha256

例如:

find . -type f \
  ! -name MANIFEST.sha256 \
  -print0 |
sort -z |
xargs -0 sha256sum \
  > MANIFEST.sha256

随后验证:

sha256sum -c MANIFEST.sha256

这样可以检测:

文件损坏
不完整复制
意外修改
磁盘错误
长期存储 bit rot

十一、最终归档还需要一个外部 SHA-256

内部:

MANIFEST.sha256

保护包里面的文件。

而最终压缩包本身还应该有:

game-migration.tar.zst.sha256

例如:

sha256sum game-migration.tar.zst \
  > game-migration.tar.zst.sha256

迁移到另一台机器之后首先执行:

sha256sum -c game-migration.tar.zst.sha256

只有 PASS 才开始解压。

这样形成两层完整性保护:

外层 archive SHA-256
        ↓
验证压缩包

内部 MANIFEST.sha256
        ↓
验证所有文件

十二、最终真正需要保留什么

完成迁移以后,长期保存其实并不需要很多文件。

每套游戏只需要:

game-migration.tar.zst
game-migration.tar.zst.sha256

因为迁移包内部已经包含:

Docker image
Compose
配置
游戏存档
mods
metadata
恢复说明
MANIFEST

打包过程产生的:

staging directory
单独的 image tar
临时 rootfs
restore-test directory
旧失败归档
审计中间文件

在确认最终 migration archive 完整以后都可以清理。


十三、README-MIGRATION 才是长期维护的关键

一个五年以后仍然能够恢复的备份,不能依赖维护者还记得当年的操作。

迁移包中应该包含类似:

README-MIGRATION.md

内容至少说明:

支持的 Linux / CPU 架构
Docker 环境要求
镜像导入方法
应用目录恢复方法
权限要求
Compose 启动方法
需要开放的端口
Master/Caves 关系
存档检查方法
日志检查方法
恢复成功判断方法

一个理想的 README 应该做到:

即使原管理员已经完全忘记部署细节,只看这个文件也能重新启动服务。


十四、端口也应该属于迁移资料的一部分

很多迁移最后失败,不是 Docker 问题,而是:

容器正常
进程正常
游戏世界正常
但客户端连接不上

原因通常只是目标服务器没有开放正确端口。

因此最终迁移资料应该根据实际配置提取:

game port
query port
Master port
Caves port
authentication port

而不是根据网上默认值猜测。

例如应该从:

Compose
server.ini
cluster.ini
游戏自身配置

读取实际配置。

这样迁移到新的服务器之后,只需要按照 README 配置:

host firewall
router/NAT
cloud security group

即可恢复外部访问。


十五、迁移成功的真正验收标准

最终不能只看:

docker load: PASS

也不能只看:

docker compose up: PASS

真正的验收标准应该是:

Docker image             PASS
配置                     PASS
数据                     PASS
权限                     PASS
容器启动                 PASS
游戏进程                 PASS
原世界识别               PASS
原存档识别               PASS
Mods                     PASS
内部 MANIFEST            PASS
最终 archive SHA-256     PASS
恢复说明                 PASS

只有这些条件真正成立,才可以说:

这个游戏服务器已经脱离原服务器,可以独立迁移。


十六、这次迁移实践带来的几个经验

1. Docker 并不自动等于可迁移

Docker 解决的是运行环境封装问题。

数据、配置、镜像版本、外部网络和恢复流程仍然需要人为管理。


2. latest 不应该成为备份策略

长期运行的服务器可能多年没有更新 image。

当前机器上的:

some-image:latest

和今天 Registry 中的:

some-image:latest

可能已经完全不是同一个东西。

因此必须保存真实 image。


3. docker export 是备用方案,不是首选方案

真正需要的是:

docker save
+
docker load

这样才能保留完整 image metadata。


4. 只有恢复过的备份才值得信任

未经恢复测试的备份只能叫:

已复制的数据。

实际启动并读取原世界以后,才有资格叫:

已验证的迁移备份。


5. 备份和退役应该是两个任务

一个常见错误是把:

备份
迁移验证
删除旧环境

放在一次操作里。

更安全的方法是:

第一阶段
审计

第二阶段
打包

第三阶段
独立恢复验证

第四阶段
确认最终归档

第五阶段
另行决定是否退役原环境

这样即使清理计划后来改变,也不会影响已经做好的迁移包。


结语

Docker 游戏服务器迁移真正需要保存的,并不是某一个目录,也不是某一个容器。

需要保存的是一套完整的运行状态:

镜像
+
配置
+
存档
+
依赖
+
恢复方法

而判断它是否完整的唯一可靠方法,是:

从备份本身真正恢复一次。

当 DST 的 Master 与 Caves 能从迁移副本重新加载原世界,当 Palworld 能重新识别已有 SaveGames,当镜像可以离线导入、配置完整、最终归档通过哈希校验时,这份备份才真正完成了从“文件副本”到“可迁移系统”的转变。

Leave a Reply

Your email address will not be published. Required fields are marked *