别再只会docker run了!这次我把Docker的“灵魂“扒给你看

发布时间:2026/6/30 1:17:12
别再只会docker run了!这次我把Docker的“灵魂“扒给你看 说起来挺讽刺的用了三年Docker我到现在才敢说自己真正搞懂了它。之前每次面试被问到“Docker镜像为什么能那么小”或者“容器删了数据还在不在”这种问题要么就是瞎扯一通什么“分层存储”要么就是顾左右而言他心里虚得很。直到有一次生产事故让我彻底清醒了——那是个深夜MySQL容器突然崩了docker rm -f 之后数据全没了。当时整个人都傻了心想这不对啊明明数据库文件都在容器里怎么会没了后来才知道容器里的“可写层”跟容器同生共死容器一没它也就跟着消失了。那次之后我痛定思痛花了整整一周时间把Docker的核心原理从头到尾研究了一遍。今天就把我的学习成果分享出来全是实打实的生产经验没有半点水分。先搞清楚一个灵魂拷问镜像到底是个什么东西我们在服务器上敲docker pull nginx的时候会看到控制台哗啦啦下载一堆东西每一行后面都跟着个 “Layer” 或者 “Digest” 之类的字眼。我当时就纳闷了一个nginx镜像不是应该就一个压缩包吗怎么搞出来这么多层后来查资料才知道Docker镜像这玩意儿根本不是一个完整的大文件它是由一堆“层”Layer叠加起来的。就像…你可以理解成是一块块乐高积木每一块都是独立的但拼在一起就能搭出一个完整的东西。这种设计有个专门的名字叫联合文件系统英文是 Union File System简称 UnionFS。那它到底是怎么工作的呢简单来说UnionFS 可以把多个不同的目录“叠”到一起让它们看起来就像是一个目录。容器里的进程压根不知道自己其实是在一堆目录的合并视图里工作它只会看到一个普普通通的文件系统。这就好比你小时候有没有玩过那种透明叠加的动画本每一页都是独立的画面但快速翻动的时候所有画面叠在一起就成了连贯的动画。UnionFS 就是这个原理只不过它是“空间上叠加”而不是“时间上叠加”。镜像的“千层饼”结构一层一层剥开看说完了基本概念咱们来看看一个典型的Docker镜像到底长啥样。通常来说一个镜像会有这么几类层最底下是基础层说白了就是一个精简过的操作系统。比如你 pull ubuntu它其实就是从这个基础层开始的。这个层里包含了最最基本的系统工具、库文件、shell 之类的。没有它后面的东西都跑不起来。基础层之上是各种中间层。每你在 Dockerfile 里写一条指令比如RUN apt-get update或者COPY ./app /usr/src/app就会在现有层的基础上再加一层。注意这里有个关键点这些层全都是只读的。一旦生成就改不了了。你要是想把某个文件改掉对不起只能在这个层的上面再加一层来“覆盖”它原来的那个文件依然存在于下层只是被“遮住”了而已。最顶上还有一层可写层但这个不是镜像的一部分是容器启动之后才会有的。我待会儿会重点讲这个。为了让大家有更直观的感受我顺手在测试服务器上跑了个命令dockerhistoryubuntu:latest输出大概是这样的IMAGE CREATED CREATED BY SIZE COMMENT a6d02b8c3f9e 2 weeks ago CMD [/bin/bash] 0B shell: /bin/bash missing 2 weeks ago ADD file:xxxxxx in / 77.8MB 构建命令可以看到这个 ubuntu 镜像基本上就两个层一个是最基础的 ADD 层77.8MB另一个是顶层的 CMD 层0B因为 CMD 本身不产生文件。写时复制这才是Docker的灵魂所在刚才说到镜像的中间层都是只读的。那问题来了如果我的容器运行过程中需要修改某个配置文件怎么办比如 nginx 容器默认的配置文件是/etc/nginx/nginx.conf但这个文件在镜像的只读层里。容器启动后我想改一下 worker 进程数Docker 难不成会把整个镜像层给改掉当然不会。这里就要引入一个核心机制——写时复制Copy-on-Write简称 CoW。它的原理说穿了很简单当容器要修改一个位于只读层的文件时Docker 并不会真的去改那个只读层的原文件。它先把这文件复制一份到顶层的可写层然后容器后续所有的读写操作都针对这个副本进行。原文件呢它还在下层好好躺着只是不再“可见”了——被上层的副本给“遮挡”住了。这个设计带来的好处太明显了第一镜像复用率极高。假设你服务器上跑了 5 个 nginx 容器、3 个 mysql 容器、2 个 redis 容器它们全都基于同一个 ubuntu 基础层。那这个 ubuntu 的文件系统在磁盘上只存了一份多出来的只是它们各自不同的部分。我在测试机上实际跑了一下基于同一个 alpine 镜像起了 10 个容器看磁盘占用dockerrun-d--nametest1 alpinesleep3600dockerrun-d--nametest2 alpinesleep3600# ... 一共起10个然后检查磁盘dockersystemdf输出显示虽然有 10 个容器在运行但镜像层占用的实际磁盘空间几乎可以忽略不计。这就是分层架构的威力。第二容器启动速度飞快。因为不需要复制整个文件系统启动容器时 Docker 只需要在上层叠一个薄薄的可写层就行耗时通常是毫秒级的。你点一下按钮容器就起来了比眨眼还快。Dockerfile 怎么写才合理别让层数害了你知道了镜像分层的原理写 Dockerfile 的时候就得讲究点了。每一条指令基本都会产生一个新的镜像层。比如你这样写FROM ubuntu:20.04 RUN apt-get update RUN apt-get install -y nginx RUN apt-get install -y vim RUN apt-get install -y curl RUN apt-get install -y git好家伙一口气 4 个 RUN 指令产生了 4 个镜像层。但问题是这些层都是不可变的啊以后你想升级某个软件或者换个版本对不起只能在后面追加新层原来的删不掉。正确的做法是合并同类项FROM ubuntu:20.04 RUN apt-get update \ apt-get install -y nginx vim curl git \ apt-get clean \ rm -rf /var/lib/apt/lists/*这样所有安装操作都压在一层里完成镜像体积更小分发起来也更快。还有一个原则把变化频繁的内容放在后面不常变化的内容放前面。你想啊镜像构建的时候是按顺序来的每一层都会被缓存。如果你把COPY ./app /app这种代码复制放在前面那每次代码改动都得重新构建后面所有的层缓存全废了。所以应该是这样FROM node:16-alpine # 先把依赖锁定版本不常变 COPY package*.json ./ RUN npm ci --onlyproduction # 再把代码放进来经常变 COPY . . CMD [npm, start]这样的顺序依赖层可以被很好地缓存只有代码改了才会触发重新构建。数据持久化才是重头戏容器删了数据还在吗好说完了镜像现在聊聊数据的事。前面提到容器启动时会在镜像层之上添加一个可写层所有容器内对文件系统的修改都写在这里。这个可写层是跟着容器走的——容器删了它也就没了。这是个大坑很多人都在这儿栽过跟头。我之前就见过有人把 MySQL 数据库直接跑在容器里数据全存在/var/lib/mysql。然后有一天服务异常他一不做二不休docker rm -f mysql-container数据直接清零GG。那怎么让数据“活久一点”呢Docker 提供了三种主流方案Volume数据卷、Bind Mount绑定挂载、还有tmpfs mount内存挂载。我重点说前两个这是生产环境最常用的。Docker Volume官方亲儿子数据安全有保障Volume 是 Docker 官方最推荐的数据持久化方式。它由 Docker 统一管理存储在宿主机的/var/lib/docker/volumes/目录下Linux 系统和容器本身是分离的。创建 Volumedockervolume create my_data查看一下dockervolumels挂载到容器dockerrun-d-vmy_data:/var/lib/mysql--namemysql-server mysql:8.0或者用--mount写法更清晰dockerrun-d\--mountsourcemy_data,target/var/lib/mysql\--namemysql-server\mysql:8.0这里-v my_data:/var/lib/mysql的意思是把my_data这个 Volume 挂载到容器里的/var/lib/mysql路径。从此以后MySQL 写入的数据全都在 Volume 里容器删了也不怕。# 删除容器dockerrm-fmysql-server# 重新起一个还是挂同一个 Volumedockerrun-d-vmy_data:/var/lib/mysql--namemysql-server mysql:8.0数据完整保留开箱即用。Volume 还有个好处是它和宿主机文件系统是隔离的不容易误操作被破坏。Docker 自己也提供了一整套 CLI 来管理它dockervolume inspect my_data# 查看详情dockervolumermmy_data# 删除卷dockervolume prune# 清理无用卷Bind Mount灵活但有代价Bind Mount 和 Volume 的本质区别在于它直接把宿主机上的某个目录映射进容器不走 Docker 的存储管理系统。dockerrun-d-v/opt/mysql/data:/var/lib/mysql--namemysql-server mysql:8.0这次/opt/mysql/data是宿主机上真实存在的路径容器里看到的就是它。这个方式的好处是直观你在宿主机上直接能找到对应的目录改配置、查日志都很方便。很多开发童鞋喜欢用这种挂载方式尤其是本地调试的时候挂个代码目录进去改完保存容器里立刻就生效了。但它的问题也很明显路径是硬编码的换台机器可能就没了Docker 对这个目录没有控制权没法做备份、迁移之类的操作权限管理也更复杂需要考虑宿主机和容器用户的对应关系所以生产环境里我建议数据库、消息队列这些有状态的服务老老实实用 Volume本地开发调试倒是可以图个方便用 Bind Mount。两种挂载方式怎么选我用血泪教训给你总结对比维度VolumeBind Mount存储位置Docker 管理路径在/var/lib/docker/volumes/宿主机任意路径管理方式Docker CLI 全套支持靠系统命令和手动操作适用场景生产环境、数据库、需要迁移备份的场景本地开发、挂载配置文件、热更新代码优点安全隔离、便于管理、支持驱动扩展直观方便、符合传统运维习惯缺点路径隐蔽、不方便直接查看移植性差、依赖宿主机环境这里有个我踩过的坑必须提一下用 Bind Mount 挂载 MySQL 数据目录的时候如果宿主机上那个目录不存在Docker 会自动创建一个空目录而不是报错。你满怀期待地启动容器结果数据库初始化在空目录上进行等发现的时候数据全乱了。Volume 就不存在这个问题创建 Volume 的时候 Docker 会帮你处理。tmpfs mount数据只存在于内存中最后简单提一下 tmpfs这是把数据存在内存里的挂载方式。速度飞快但容器一停数据就丢了。dockerrun-d--tmpfs/run:rw,noexec,size64m--nametest-container alpine适合存一些临时文件、session 数据之类的不需要持久化的东西。比如跑一些跑完就不要的临时计算任务用 tmpfs 可以避免磁盘 IO 开销。总结一下记住这几点就够了Docker 的镜像分层和持久化机制其实就围绕两个核心问题一个是镜像怎么存靠 UnionFS 把一堆只读层叠在一起用写时复制CoW实现按需复制既省空间又保证性能。写 Dockerfile 的时候注意合并命令、合理安排层顺序能让你的镜像更轻量、构建更快。一个是数据怎么活容器的可写层跟着容器走删容器就丢数据。想让数据活下来要么用 Volume 交给 Docker 管理要么用 Bind Mount 直接挂载宿主机目录。生产环境首选 Volume本地开发可以图方便用 Bind Mount。好了关于 Docker 的核心原理和数据持久化就说这么多。写这篇文章的目的就是想让大家不要再像我当年一样只会docker run和docker pull遇到问题两眼一抹黑。技术这东西吧光会用是不够的知道背后的原理才能用得更稳、出问题的时候也更有底气。如果觉得这篇文章对你有帮助欢迎转发给身边做运维或后端的朋友大家一起来交流学习。