Docker 传统构建器在 VOLUME 后写入文件,最终镜像却不保留
限定传统构建器对 VOLUME 后修改的丢弃行为,采用 BuildKit 或调整自有声明顺序,并排除旧数据卷遮盖。
Dockerfile 的 RUN 已成功写入文件或修改目录属主,启动新镜像后,文件却不见了,属主也回到了原值。如果这些操作位于 VOLUME 声明之后,而且构建使用的是传统构建器,需要先检查这个组合。
传统构建器会丢弃声明 VOLUME 后对相应目录的构建期修改;BuildKit 会保留这些修改。因此,同一份 Dockerfile 换了构建后端,产物可能不同。不能把“VOLUME 后的改动一定丢失”当成所有 Docker 构建的规则。
本文针对 Linux 镜像和仍在使用传统构建流程的项目。当前常规 Docker 构建使用 BuildKit;传统 Linux 构建路线已弃用,不应为了重现问题重新启用它。
先分清构建期丢失和运行时遮盖
例如这个测试片段把文件写入放在声明之后:
FROM alpine:3.22
RUN mkdir -p /demo-data
VOLUME ["/demo-data"]
RUN printf '%s\n' 'example-content' > /demo-data/marker.txt
检查实际执行构建的脚本、CI 环境及日志。显式设置 DOCKER_BUILDKIT=0 是需要关注的线索;不要只查看自己的终端,因为包装工具可能在子进程中选择另一种构建方式。
同时确认 VOLUME 是否来自基础镜像。在自己写的 Dockerfile 中没找到声明,并不代表父镜像没有声明。可对实际基础镜像做只读检查,名称换成项目正在使用的版本:
docker image inspect --format '{{json .Config.Volumes}}' your-base-image:version
另一种常见情形是镜像文件本来正确,但运行容器挂了旧命名卷或宿主目录,遮盖了镜像内容。若不挂旧数据时文件存在,应处理挂载与数据更新,不属于这里的传统构建器问题。不要通过删除生产卷来区分这两种情形。
优先让项目明确使用 BuildKit
如果原项目没有必须保留传统构建器的约束,在隔离构建环境确认 Buildx 可用,再用明确的 BuildKit 入口生成单平台测试镜像:
docker buildx build --load --progress=plain -t volume-build-check:review .
--load 把这个单平台构建结果载入本机镜像存储,便于后续检查。命令中的当前目录、Dockerfile、构建参数和目标阶段,都要与待检查项目一致;这里不能替代多平台镜像原有的发布流程。
若由 CI 或其他工具代为构建,要修改真正调用构建器的入口。只在本地得到正确产物,却让发布任务继续走传统后端,线上仍会得到另一种结果。切换后需要完成原有应用检查,不能只比较一个文件就直接替换生产镜像。
必须兼容旧流程时,把初始化移到声明之前
当这条 VOLUME 由自己维护的 Dockerfile 声明,并且基础镜像尚未把该目录声明为卷,可以调整顺序:
FROM alpine:3.22
RUN mkdir -p /demo-data \
&& printf '%s\n' 'example-content' > /demo-data/marker.txt
VOLUME ["/demo-data"]
需要写入的初始文件和目录属性先完成,再声明卷。这保持了运行时需要卷的设计,并让传统构建器能够保存声明前的内容。只把两个后续 RUN 合并,仍然放在 VOLUME 后面,不会改变这个边界。
如果声明已经在父镜像中,仅将子 Dockerfile 的 VOLUME 移到末尾无效:进入子阶段时该目录就已被声明。应采用支持的 BuildKit 流程,或由负责基础镜像的团队调整其构建定义;不要把删除镜像元数据、重打包整个根文件系统当作常规修复。
用新建的测试容器检查初始内容
对前面的无业务数据示例,可以这样验证:
docker run --rm --network none --entrypoint /bin/sh \
volume-build-check:review \
-c 'test -f /demo-data/marker.txt && cat /demo-data/marker.txt'
不要给这次检查挂入已有业务卷或宿主目录。镜像的 VOLUME 会让新容器取得新卷,并用镜像对应位置的初始内容初始化;示例应能读到 example-content。检查使用的是专门的测试标签,不能改成清空旧卷后再看结果。
真实项目还要核对所需文件、数字 UID/GID、权限及应用启动状态。如果已经确认用 BuildKit 构建,新建测试容器仍缺文件,就应继续检查阶段选择、复制路径和实际镜像版本,不能继续套用传统构建器的丢弃规则。
修复镜像不会自动更新已经使用过的数据卷。发布前保留原构建定义和已验证镜像,数据目录变化按应用的迁移流程处理。回退时恢复原定义与镜像版本;不要清理共享卷,也不要让构建器切换承担数据恢复的职责。


