侧边栏壁纸
博主头像
qiql博主等级

水能载舟,亦可赛艇

  • 累计撰写 34 篇文章
  • 累计创建 28 个标签
  • 累计收到 20 条评论

目 录CONTENT

文章目录

Docker 容器可写层迁移方案

qiql
2026-08-19 / 0 评论 / 0 点赞 / 3 阅读 / 1,401 字

1. 背景

Docker 容器创建后,端口、GPU 数量等配置通常无法直接修改,只能重新创建容器。

常规方案是先执行 docker commit 保存镜像,再创建新容器。但这样会把原容器的文件变更写入只读镜像层,导致新容器的 --storage-opt size 只能限制新写入层,无法限制整个环境实际占用的空间。

本文提供一种适用于 overlay2 的解决方案:

创建带有新配置和容量限制的容器,然后将旧容器的可写层直接迁移到新容器。

该方案可以同时实现:

  • 保留容器根目录中的完整环境。
  • 修改 GPU、端口等创建期配置。
  • 保持容器根目录写入层的容量限制。
  • 避免生成体积巨大的中间镜像。

2. 实现原理

使用 overlay2 时,容器根文件系统由两部分组成:

  • LowerDir:基础镜像的只读层。
  • UpperDir:当前容器产生的新增、修改和删除记录。

新容器如果使用完全相同的基础镜像,其 LowerDir 与旧容器一致。因此,只需要将旧容器的 UpperDir 内容迁移到新容器的 UpperDir,即可恢复原容器的根文件系统状态。

新容器在创建时指定:

--storage-opt size=100G

Docker 会为新容器的读写层设置 XFS project quota。解压到该目录中新创建的文件会继承对应的 project ID,因此仍然受新容器容量限制。

3. 使用条件

该方案需要满足:

  • Linux Docker Engine。
  • 使用经典 overlay2 存储驱动。
  • Docker 数据目录位于启用了 pquota 的 XFS 文件系统。
  • docker inspect 能看到 GraphDriver.Data.UpperDir
  • 新旧容器使用完全相同的镜像 ID,而不只是相同的镜像标签。
  • 操作时旧容器已经停止,新容器从未启动。

Docker Engine 29 以后,全新安装可能默认使用 containerd image store,此时本方案不能直接套用。

4. 操作流程

4.1 获取旧容器信息

OLD_CONTAINER=old-container

docker inspect -f '{{.Image}}' "$OLD_CONTAINER"
docker inspect -f '{{.GraphDriver.Data.UpperDir}}' "$OLD_CONTAINER"

记录:

  • 镜像 ID。
  • 旧容器 UpperDir 路径。
  • 启动命令、环境变量、端口、挂载目录、网络和重启策略。

4.2 创建新容器

使用旧容器的精确镜像 ID 创建新容器,并设置新的 GPU、端口和容量限制:

docker create \
  --name new-container \
  --gpus 2 \
  -p 8080:80 \
  --storage-opt size=100G \
  IMAGE_ID \
  ORIGINAL_COMMAND

获取新容器写入层:

docker inspect -f '{{.GraphDriver.Data.UpperDir}}' new-container

此时不要启动新容器。

4.3 停止旧容器和 Docker daemon

docker stop old-container
sudo systemctl stop docker

旧容器必须停止,否则归档期间仍可能发生文件变化,导致数据不一致。

如需让其他容器在 dockerd 停止期间继续运行,可提前在 /etc/docker/daemon.json 中启用:

{
  "live-restore": true
}

然后重新加载配置:

sudo systemctl reload docker

live-restore 只能保持其他容器继续运行,不会为运行中的容器热更新 GPU。

4.4 归档旧写入层

必须保留所有权、ACL、硬链接、特殊文件和扩展属性:

sudo tar \
  --numeric-owner \
  --xattrs \
  --xattrs-include='*' \
  --acls \
  --sparse \
  -C "$OLD_UPPER" \
  -cpf /backup/container-upper.tar .

不能使用 ZIP,因为 OverlayFS 使用 whiteout 和 trusted.overlay.* 扩展属性记录文件删除及目录覆盖状态。

4.5 恢复到新写入层

确认目标 UpperDir 属于新容器且内容为空,然后执行:

sudo tar \
  --numeric-owner \
  --xattrs \
  --xattrs-include='*' \
  --acls \
  --sparse \
  -C "$NEW_UPPER" \
  -xpf /backup/container-upper.tar

不要复制以下目录或文件:

  • merged
  • work
  • lower
  • link

只迁移 UpperDir,通常对应 overlay2 层目录中的 diff

4.6 启动并验证

sudo systemctl start docker
docker start new-container

验证:

docker exec new-container nvidia-smi
docker exec new-container df -h /
docker inspect new-container

还应在测试环境中执行受控写入测试,确认达到容量限制后返回 ENOSPC

5. 注意事项

  • 不要仅凭镜像标签判断基础镜像相同,必须比较镜像 ID。
  • 不要在容器运行时复制 UpperDir
  • 必须以 root 身份归档,否则可能丢失 whiteout、所有权和特殊文件。
  • SELinux 环境需要额外检查或重新设置目标容器文件标签。
  • 如果归档内容超过新容器额度,解压会中途失败。此时应删除新容器并重新创建,不要继续使用不完整的写入层。
  • 在确认新容器正常之前,不要删除旧容器和归档文件。
  • 直接操作 /var/lib/docker 不属于 Docker 官方支持接口,升级 Docker 后需要重新验证。

6. 结论

该方案通过分离“容器运行配置”和“容器可写层状态”,实现了:

原基础镜像
    +
原容器 UpperDir
    +
新 GPU、端口等配置
    +
新容器写入层容量限制

它解决了 Docker 官方接口无法同时完成的三个需求:保留根目录环境、修改创建期配置、继续限制容器写入层容量。

参考资料:

0

评论区