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
不要复制以下目录或文件:
mergedworklowerlink
只迁移 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 官方接口无法同时完成的三个需求:保留根目录环境、修改创建期配置、继续限制容器写入层容量。
参考资料:
评论区