昨天我遇到了一个奇怪的问题,让我对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时,我不再只是输入命令,而是在脑海中能看到那些透明的文件系统层,一层层叠加,最终构成我的容器视图。这种感觉,就像是获得了透视容器内部的超能力。




