Docker容器层揭秘:修改为何消失?
昨天我遇到了一个奇怪的问题,让我对Docker的理解发生了根本性的改变。
事情是这样的:我正在做一个Python机器学习项目,按照教程用Docker打包应用。一切都顺利——镜像构建成功,容器运行正常。但当我尝试查看容器里的文件时,发现了一件不可思议的事情。
问题浮现
我构建的镜像是基于python:3.12-slim,然后在里面安装了numpy、pandas等库,还复制了我的机器学习代码。但当我运行docker run my-ml-app ls /app时,看到了我的文件;而当我进入容器修改文件后,重新运行相同的命令,之前的修改竟然“消失”了!
这太反直觉了。我明明修改了文件,为什么下次运行看到的还是原始版本?难道Docker有什么魔法吗?
第一反应:是不是我操作错了?
我开始怀疑自己。是不是我没有正确保存?或者容器重启后状态丢失了?我重复实验了几次:
-
进入容器,创建一个新文件
-
退出容器
-
再次运行容器,查看文件是否存在
结果每次都一样——文件不见了。这让我困惑不已。如果容器不能持久化数据,那它还有什么用?
深入调查
我开始查看Docker文档,偶然发现了“容器层”和“镜像层”的概念。原来,容器运行时,Docker会在镜像层之上创建一个可写层。所有修改都发生在这个可写层,而原始镜像层是只读的!
这解释了为什么我的修改“丢失”了——因为每次运行新容器时,都会创建一个新的可写层。但为什么会有这种设计?这看起来多此一举啊。
实践验证
为了验证这个理论,我设计了一个实验:
bash
# 创建一个测试镜像 echo "Hello from base layer" > file.txt cat > Dockerfile << EOF FROM alpine COPY file.txt / EOF docker build -t test-layer . # 运行容器并修改文件 docker run -it test-layer sh # 在容器内:echo "Modified content" > /file.txt # 退出容器 # 再次运行查看 docker run test-layer cat /file.txt # 输出:Hello from base layer
实验结果证实了我的理解:镜像层是只读的,容器层的修改不会影响镜像。但这带来了新的疑问——如果我想保存修改怎么办?
发现持久化方案
继续研究,我找到了两种解决方案:
-
docker commit:把容器层保存为新镜像
-
数据卷(Volume):将主机目录挂载到容器
我测试了第一种方法:
bash
# 运行容器并修改 docker run -it --name test-container test-layer sh # 修改文件后退出 # 提交为新镜像 docker commit test-container modified-image # 运行新镜像查看 docker run modified-image cat /file.txt # 输出:Modified content
成功了!但这方法有个问题:如果我在容器里安装了很多软件,做出了很多修改,提交的镜像会包含所有中间文件,导致镜像臃肿。
遇见UnionFS
这时我遇到了UnionFS(联合文件系统)这个概念。UnionFS允许将多个目录(称为“分支”)透明地叠加在一起,形成一个统一的视图。在Docker的语境中:
-
每个Dockerfile指令创建一个新的层
-
基础镜像是最底层
-
后续的RUN、COPY等指令创建叠加层
-
运行容器时,在最上面添加可写层
我决定亲手验证UnionFS的工作原理。在Linux系统上,我做了这个实验:
bash
# 创建测试目录 mkdir unionfs-test && cd unionfs-test mkdir lower upper work merged # 在lower层创建文件 echo "I am from lower layer" > lower/file.txt echo "Only in lower" > lower/lower-only.txt # 挂载UnionFS(使用overlay) mount -t overlay overlay -o lowerdir=lower,upperdir=upper,workdir=work merged # 查看合并后的视图 cat merged/file.txt # 输出:I am from lower layer ls merged/ # 能看到所有文件 # 在merged中修改文件 echo "Modified in merged view" > merged/file.txt # 检查各层变化 cat lower/file.txt # 还是原始内容 cat upper/file.txt # 是修改后的内容! ls upper/ # 发现file.txt
这个实验让我恍然大悟!UnionFS的写时复制(Copy-on-Write)机制意味着:
-
读取文件时,从上往下查找,找到即返回
-
修改文件时,如果文件在只读层,会先复制到可写层再修改
-
删除文件时,在可写层创建“白化”(whiteout)标记
Docker存储驱动的选择
进一步研究发现,Docker支持多种存储驱动:aufs、overlay2、devicemapper等。overlay2是目前默认的,因为它:
-
性能更好
-
支持最多128个下层
-
与Linux内核集成更紧密
我检查了我的Docker配置:
bash
docker info | grep "Storage Driver" # 输出:Storage Driver: overlay2
解决实际问题
回到最初的问题——如何在我的机器学习项目中持久化数据?我现在明白了最佳实践:
-
训练数据:使用数据卷挂载
bash
docker run -v /host/data:/app/data my-ml-app
-
模型文件:输出到挂载卷或推送到模型仓库
-
代码修改:应该修改Dockerfile重建镜像,而不是在容器内直接改
心得与反思
这次探索让我深刻理解了几个关键点:
-
不可变基础设施:容器镜像应该是不可变的,这确保了环境一致性
-
关注点分离:代码和配置应该与数据分离
-
分层的好处:共享基础层节省存储,分层构建加速CI/CD
最让我震撼的是,这种看似“麻烦”的设计,实际上是经过深思熟虑的。容器层和镜像层的分离,强制我们思考什么是应该打包进镜像的(代码、依赖),什么是应该外置的(数据、配置)。
我也意识到,学习技术不能停留在表面。如果不理解UnionFS,我就无法真正理解Docker的文件系统行为,可能会误用或滥用容器技术。
给初学者的建议
如果你刚开始学习Docker,我建议:
-
先理解镜像和容器的区别
-
动手实验UnionFS的挂载和修改
-
学习使用数据卷进行持久化
-
思考你的应用哪些部分应该进镜像,哪些应该外置
这条路我走过,虽然有些曲折,但每一步的发现都让我对容器技术有了更深的理解。容器不仅仅是“轻量级虚拟机”,它的设计哲学和实现机制,都值得我们深入探究。
现在当我使用Docker时,我不再只是输入命令,而是在脑海中能看到那些透明的文件系统层,一层层叠加,最终构成我的容器视图。这种感觉,就像是获得了透视容器内部的超能力。








