
1. 镜像和容器说白了就是一张图纸和无数栋楼的关系我见过太多刚接触 Docker 的人卡在同一个地方明明已经docker pull拉了一个镜像也照着教程docker run跑起来了可真要他自己解释一句“镜像和容器到底有什么区别”又说不清楚。最典型的表现就是——在容器里改了文件然后到处问“为什么我的镜像没变”“容器删了数据还在不在”“我能不能把一个容器直接拷给同事”这些问题归根到底就是没把镜像和容器的关系吃透。先说最直观的理解方式镜像是一张建筑施工图纸容器是照着图纸盖出来的那一栋楼。图纸不因任何一栋楼而被改变同一张图纸可以盖出无数栋结构相同但独立存在的楼容器就是那个“独立存在”的运行实例它有自己的状态有自己的门窗、家具、住户但这些全都建立在图纸规定的框架之上。很多热词里提到的“linux镜像安装”“ubuntu官网镜像下载”“win11镜像下载”其实是操作系统安装包 ISO和 Docker 镜像是两码事后面我会专门讲。先记住这句话容器是由镜像创建的运行实例镜像是容器启动时的只读模板。我平时排查问题时发现绝大多数概念混淆都源于三个根本点没理清镜像只读、容器可写、一个镜像可以起多个容器。这三个点只要心里有数后面的所有操作都不会乱。1.1 一个镜像能起多少个容器理论上没有上限同一份 MySQL 8.0 镜像你可以在一台服务器上起三个容器3306、3307、3308 三个端口各一个它们之间互不干扰各自拥有独立的数据文件、独立配置文件、独立日志。这在生产环境里很常见比如同一个镜像要分别部署到测试、预发、正式环境或者要给不同租户做数据库实例隔离都是“一套镜像、多套容器”的玩法。之所以能做到这一点得从文件系统层面理解。每个容器启动时Docker 会在镜像的基础上再叠加一层“容器层”这层是空的、可写的。容器对文件的所有增删改都只会落在这个容器层里镜像本身始终是那个只读的“底稿”。这就像你在同一张设计图上复印了十份每份上面都可以用铅笔随意涂改但原件永远是干净的。也正是这个机制让“容器多了会不会占很大磁盘空间”这个问题有了答案不会。多个容器共享同一个镜像的只读层磁盘上只存一份镜像数据每个容器额外占用的只是自己那个可写层的容量。我之前见过有人担心十个容器会把磁盘撑爆实际算下来十个容器加一起的额外开销可能都不到几十 MB前提是你没有往里面写大文件。1.2 核心区别只有四个字只读与可写我用一张表把这俩的核心差异列清楚新手照着记基本就不会混淆了维度镜像Image容器Container本质静态的只读模板镜像运行后的动态实例文件系统只读层不可直接修改在镜像上叠加可写层可自由修改生命周期不随容器启停而改变长期存在随 start/stop/rm 而启停或消亡占用的资源只占磁盘空间占用 CPU、内存、网络、磁盘同一个镜像一份可变出多个互不干扰的实例操作命令pull/build/push/tagrun/start/stop/rm/exec能不能直接改名可以打 tag通过docker rename改名这张表你不需要背但心里一定要有。特别是“镜像不可直接修改”这条很多人刚学的时候会在容器里装了一堆软件后跑到镜像目录去找文件发现找不到然后又跑回来问我“是不是装错地方了”。不是的你装的东西都在容器的可写层里镜像目录里当然没有。1.3 别把“Docker镜像”和“系统ISO镜像”混为一谈热搜词里出现了“linux镜像安装”“ubuntu官网镜像下载”“win11镜像下载”“centos7镜像下载官网”这些。这些说的是操作系统安装包一张 Windows 11 的 ISO、一个 Ubuntu 的安装光盘镜像确实是“镜像”二字但和 Docker 镜像完全是两个物种。操作系统 ISO 是用来装物理机或虚拟机的它包含完整的引导程序、内核、驱动、安装器。Docker 镜像则是用来“生成容器”的它本质上不是一个完整的操作系统只是一套完整的应用运行环境。一个 CentOS 的 Docker 镜像大约只有二百来 MB而 CentOS 的完整 ISO 要好几个 GB就是因为 Docker 镜像里没有内核、没有引导程序它借用的是宿主机内核只是把用户态的文件系统、库、应用打完包。这意味着一个很反直觉的事实你可以在 Windows 的 Docker Desktop 里跑一个 Ubuntu 容器但这个容器用的内核其实是 Windows 自带的 WSL2 里的 Linux 内核。你跟别人说“我在 Windows 上跑了个 Ubuntu”严格来说你跑的是 Ubuntu 的用户态环境而不是一套完整的 Ubuntu 操作系统。这个概念不搞清楚后面看容器的资源限制、安全隔离都会觉得别扭。2. 镜像为什么天生是分层的这是容器能飞快启动的根基镜像和容器在物理上的关系已经说清了接下来得回答一个更底层的问题镜像内部到底是什么结构才让“复制出无数容器且相互独立”这件事这么便宜、这么快。很多教材喜欢直接抛“镜像是由多层只读层叠加而成的”但没有解释为什么要有层。我打个比方盖楼的时候地基是一层结构是一层外墙是一层装修是一层。如果每次交付一位业主都要从地基开始重新建一栋一模一样的楼那代价不可想象。Docker 的思路是这些层全部共享新业主只需要在自己的户型层上做点缀就行。2.1 联合文件系统多层目录拼成一个完整视图Docker 镜像的分层底层靠的是联合文件系统UnionFS现在主流宿主机上默认用的是 OverlayFS。它的核心能力是把多个不同的目录“叠放”在一起对外暴露成一个统一的文件系统视图。你写一个 Dockerfile里面通常有好几条指令比如FROM centos:7 RUN yum install -y net-tools COPY app.jar /app/app.jar CMD [java, -jar, /app/app.jar]每一条RUN、COPY、ADD指令都会生成一个新的只读层。第一层是基础镜像centos:7的全部文件第二层记录的是“安装 net-tools 之后文件系统相比第一层多出来的那些文件”第三层记录的是“拷贝 app.jar 之后新增的文件”。每一层只保存增量。多个只读层叠加后从容器里看是一个完整的文件系统既有 CentOS 的基础目录又有 net-tools又有 app.jar。但每一层各自独立存放互不影响。这也解释了为什么修改容器里的文件那么“轻”读文件时Docker 从上往下逐层查找写文件时Docker 不会去改底层只读文件而是在最上层的可写层新建一个副本然后读写这个副本。2.2 写时复制不复制就永远不会慢“写时复制”这个概念很多学过 Linux 或学过程序员内存管理的人会觉得眼熟它在 Docker 里也一样关键。镜像的只读层是可读的但一旦容器要修改某一个底层文件Docker 会将这个文件从只读层复制一份到可写层然后对副本做修改。底层文件始终保持原样。所以如果容器只是读文件、跑程序它几乎不增加额外存储开销。只有真正写操作发生才会复制文件到可写层。这也是为什么同时启动几十个容器不会像你想象中那样瞬间耗尽磁盘——绝大多数场景下它们只是在“读同一本原件”。不过要提醒一句写时复制节省的是“时间”和“空间”但如果你在容器里写大量频繁变动的文件比如直接把 MySQL 的数据文件放在容器可写层那性能会比挂载数据卷差一些后续我会在 MySQL 实战部分细说。2.3 容器删除可写层跟着消失既然可写层是容器独有的那容器的生命周期就决定了数据的安全性。docker rm删除容器时那个可写层也会一并被删除里面所有“容器运行期间写入的数据”就没了。这句话说出来很多新手觉得“这不是废话吗”但实际踩坑的人前赴后继。最典型的就是用 Docker 跑 MySQL 时不挂载数据卷跑了两个月数据库里几十 GB 业务数据某天误操作docker rm一下数据直接清零。镜像还在容器还能重新起BUT 数据库里的一切都没了。正确做法在前面已经埋了伏笔把“容器可写层里需要持久化的数据”放到宿主机目录或数据卷里通过-v参数把宿主机目录挂载进去。这样容器删了重起数据仍然在宿主机的挂载目录里。判断一个东西该不该放容器里的标准很简单删掉这个容器你会不会心疼会心疼就别放可写层放数据卷。这句话我几乎在每次答疑里都要重复一遍。3. 镜像与容器不是单向关系build、commit、import/export 全链路理解了“镜像造容器”之后就该看反方向的操作了如何从容器里产出新的镜像以及镜像文件如何在机器之间搬运。这一步是很多人从“会用”过渡到“会造”的关键。3.1 Dockerfile 是“图纸的图纸”最推荐的正规路前面提过镜像是多层只读层拼接出来的。怎么拼最标准的方式是写 Dockerfile然后用docker build去构建。docker build -t myapp:v1 .这里的“.”指的是构建上下文目录Docker 会把该目录下的所有文件打包传给守护进程用于后续构建。一个常见坑是在项目目录里docker build但目录里有个超大无关文件夹没排除构建过程就奇慢无比。解决办法是写.dockerignore文件把node_modules、.git、target这些排除掉有点像.gitignore的作用。在 Dockerfile 里层级划分直接影响镜像体积。经验法则是把变化频率小的指令放前面变化频率大的放后面充分利用层缓存多个RUN尽量通过合并减少不必要的层安装完软件包后及时清理缓存文件比如yum clean all、rm -rf /var/cache/apt/*。这样做出来的镜像更小、构建更快、复用性更好。Dockerfile 构建出来的镜像天然具有“可复现”特性任何人拿到同一份 Dockerfile构建出的镜像基本一致。这对交付和协作特别重要。3.2 docker commit应急可以别当作主力相比 Dockerfile 的“正规军”docker commit更像是“战地急救包”。它的作用是当前容器的可写层 底层只读层打包成一个新镜像。docker commit my_container myapp:hotfix-20250301什么时候用它比如你在容器里手动调试了半天终于调通了某个配置想快速把状态保存下来避免容器重起后配置丢失。又或者你在容器里临时装了几个包来不及写 Dockerfile想先打包一份镜像出去应急docker commit都够用。但我一直不建议把 commit 作为主力镜像生产方式原因有两个不可复现镜像怎么产生的、当时装了什么、改了什么全在容器状态里历史完全不可追溯。一旦镜像丢了光靠 commit 产物很难还原过程。镜像体积容易膨胀容器可写层往往带着大量临时文件、日志、包管理缓存commit 会把这些全部纳入镜像越积越脏。所以我的习惯是可以用 commit 做“现场快照”但随后必须根据现场操作反推出一份 Dockerfile用正经方式重新构建一版干净镜像。3.3 save/load 与 export/import别再用错这两组命令都用来迁移镜像但效果完全不同这里值得单拎出来说。命令作用对象保留镜像层结构和历史吗典型使用场景docker save/docker load镜像保留全部层和历史同架构机器之间完整迁移镜像或离线分发docker export/docker import容器文件系统不保留层只有文件系统快照只看容器文件内容、交给其他工具处理docker save导出的是一个压缩包里面包含这个镜像的所有只读层记录和元数据docker load可以原样恢复成一个可用镜像docker run没问题。docker export导出的则是容器当前整个文件系统的扁平快照它已经没有“镜像”的结构了docker import导入后得到的镜像层历史完全丢失体积也往往很大。简单记要“完整镜像搬家”用 save/load只是“把容器文件打包带走”用 export/import。经常有人把这两个混着用结果 load 的时候报格式错误或者镜像跑不起来问题大多出在这。4. 镜像与容器关系实战MySQL、Redis 主从、青龙面板挨个走一遍概念讲再多不如上手跑几个场景。我挑的三个例子不是随意选的它们分别代表了镜像与容器关系里三个最典型的应用面数据持久化、多容器协同、镜像升级带来的依赖问题。4.1 MySQL 8.0不挂数据卷的容器都活不过一次 rm先用 MySQL 8.0 演示最基本的“镜像 数据卷 端口映射”组合。我推荐的启动命令长这样docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD你的密码 \ -v /opt/mysql8/data:/var/lib/mysql \ -v /opt/mysql8/conf:/etc/mysql/conf.d \ --restartalways \ mysql:8.0重点看-v两个挂载。把容器里 MySQL 的数据目录/var/lib/mysql挂到宿主机/opt/mysql8/data这样无论容器怎么删、怎么重起数据文件都留在宿主机磁盘上。配置文件也同理你可以直接在宿主机上改my.cnf重启容器即可生效不用进容器里折腾。我见过太多“docker 安装 mysql 失败”的求助帖最后发现十有八九是这三类问题没挂数据卷容器一删数据全没追悔莫及端口映射冲突3306 被宿主机已有 MySQL 占用了容器起不来字符集没配进去后中文全变问号。第一条最致命。哪怕你只是本地测试也养成“关键数据一律挂卷”的习惯。4.2 Redis 主从一份镜像三个容器构成一套集群Redis 主从是“同一镜像起多个容器”的绝佳演示。主从关系不是镜像之间的区别而是容器启动参数和配置的区别。# 主节点 docker run -d --name redis-master -p 6379:6379 redis:7 redis-server --appendonly yes # 从节点1 docker run -d --name redis-slave1 -p 6380:6379 redis:7 redis-server --slaveof 目标IP 6379 # 从节点2 docker run -d --name redis-slave2 -p 6381:6379 redis:7 redis-server --slaveof 目标IP 6379三个容器用的是同一个redis:7镜像但各自有不同的容器名、不同端口、不同启动参数。容器名和端口就是“同一张图纸盖出的三栋楼的门牌号和地址”。这里面最容易踩的坑是主从之间用localhost通信。在容器环境下每个容器都认为自己是localhostredis-slave1 里的localhost指向的是它自己永远连不上主节点。必须写宿主机的局域网 IP或者通过--network让容器处于同一自定义网络里用容器名互相访问比如--slaveof redis-master 6379。这个坑理解起来特别简单三栋楼虽然是照着同一张图纸盖的但每栋楼的“一楼门厅”都是各管各的没有哪栋楼的“一楼门厅”能代表整个小区。4.3 青龙面板与依赖管理容器升级后镜像层面的“搬家”问题青龙面板这类应用很多人用 Docker 跑但升级镜像时经常会遇到一个经典问题升级后依赖丢失脚本跑不了。原因拆开看就很清楚青龙的 JS/Python 依赖安装在容器的可写层里。你docker pull了新版本的青龙镜像然后用新镜像重新创建容器新容器基于新镜像的只读层完全没有你旧容器里手动安装的那些依赖看起来就是“升级后依赖全没了”。这是“镜像和容器生命周期”在真实世界里最直白的体现。依赖装在哪一层决定了它会不会随着容器删除而消失。解决办法无非两个方向依赖固化进镜像把依赖安装写进 Dockerfile通过自定义 Dockerfile 重新构建镜像这样新容器基于新镜像启动时依赖天然就在只读层里依赖放在挂载卷里把依赖目录通过-v挂出来新容器挂载同一个宿主机目录依赖就不会因容器替换而丢失。我更推荐前者因为镜像本身可复现团队里任何人拉下来都是同一套环境。如果你的场景是“必须用现成镜像”那就至少要把依赖目录挂卷出来。总原则还是那句会心疼的数据就不要放在容器可写层里。5. 容器和镜像“分家”之后我遇到过的几个高发故障这节聊的全是我自己或身边同事在实际环境里真踩过的坑按出现频率排序。每一个坑如果你把“镜像只读、容器可写、容器共享镜像”这组关系想清楚基本都能自己推出答案。5.1 Docker Desktop 起不来virtualisation support 未检测到很多人在 Windows 上装 Docker Desktop启动时看到Docker Desktop failed to start because virtualisation support wasnt detected第一反应是怀疑 Docker 有问题但 Docker 只是“报告问题的人”。它的意思是容器需要 Linux 内核级的虚拟化支撑而你的 Windows 环境把虚拟化关掉了。排查链路一般是进 BIOS/UEFI确认 Intel VT-x 或 AMD-V 已开启在 Windows 功能里确认“适用于 Linux 的 Windows 子系统”和“虚拟机平台”两个选项已勾选如果之前装过旧版 Hyper-V或者系统曾处于开发者模式可能需要重启后再试。Docker Desktop 在 Windows 上依赖 WSL2 或 Hyper-V 提供内核本质上就是给容器提供“盖那栋楼的地基”。地基没打图纸再完美楼也立不起来。5.2 容器内置 CentOS 7.9sshd 启动失败有人在做“容器内跑 sshd”时踩坑现象是systemctl start sshd或直接启动sshd总失败。这里要往深处想一层很多官方镜像默认没有 systemd甚至没有 init 进程管理。在容器里运行systemctl大概率报错“System has not been booted with systemd as init system (PID 1)”。这不是 sshd 本身坏了而是你错误地把“物理机/虚拟机习惯”直接搬进容器。处理办法有两个方向不依赖 systemd直接手工执行sshd或者用service sshd start前提是镜像装了对应的服务脚本如果一定要 systemd 管理多个服务那就选用带 systemd 的基础镜像或者在启动容器时额外配置特权模式但这会让镜像变大、隔离性变差一般不建议。我的建议是在容器里不要按“多服务常驻”的思路设计。一个容器装一个主进程是 Docker 生态的主流用法。你如果必须同时跑 sshd 主应用要么用docker exec进容器调试而不是开 sshd要么考虑拆分容器。镜像和容器的设计意图本来就是让“一栋楼一层一个功能”而不是把整栋楼搞成一个大杂烩。5.3 宿主机访问不到容器内的 MySQL容器起来了docker ps正常但宿主机上mysql -h 127.0.0.1 -P 3306连不上。排查思路分几步看端口映射docker ps里应有0.0.0.0:3306-3306/tcp如果没有-p 3306:3306就没暴露宿主机自然访问不了看容器状态docker logs mysql8里有没有“ready for connections”的日志看容器 IP 模式如果容器用了host网络模式端口直接就打在宿主机监听上了不再需要-p映射最后看 MySQL 的 bind-address默认对容器外部连接有时只监听 127.0.0.1需要调整配置。这类问题真正难的不是某一步而是新手上路时常常在“容器里看得到宿主机看不到”这道坎上绕圈子。本质上容器是有自己网络命名空间的宿主机端口和容器端口是两个世界-p就是在两个世界之间开一扇门。5.4 宝塔面板里某个容器要使用宿主机的网络环境有网友问“宝塔内某个容器让他使用宿主机的网络环境”其实说的就是 Docker 的--network host模式。默认情况下容器用 bridge 网络有自己的 IP 和虚拟网卡和宿主机是“两栋楼中间通过网线连接”。而--network host模式下容器直接共享宿主机网络栈相当于容器里的进程“住进了宿主机这栋楼的房间”端口监听直接在宿主机上不经过任何 NAT 或端口映射。docker run --network host -d --name myapp myimage这个模式的优缺点要心里有数好处网络性能几乎没有损耗宿主机上能访问的 IP 和端口容器里也能直接访问配置 Kafka、Redis 等需要与宿主机大量交互的服务很方便坏处容器隔离性下降端口冲突风险大。“什么时候选 bridge、什么时候选 host”没有绝对标准。我自己的经验是生产环境的微服务尽量走 bridge 自定义网络用服务名互相调用可移植性强只有本地调试或对网络延迟极度敏感的场景才考虑 host。5.5 权限问题容器内文件所有者和宿主机用户对不上容器里创建的文件经常在宿主机上是 root 所有或者宿主机用户账户在容器里访问不了。特别常见于挂载卷容器以 root 跑写出来的文件归 root宿主机上你用自己的普通账号想去改发现没有权限。解决思路一般有启动容器时指定用户docker run --user 1000:1000让容器内进程以宿主机账户的 UID/GID 运行镜像构建时提前创建对应 UID 的用户而不是盲目用 root在容器内外共用挂载目录时尽量统一 UID/GID。这个坑不解决后面做 CI、做文件交换时会在“文件权限拒绝”上浪费很多时间。6. 镜像安全和容器安全名字听起来像加固思路完全不一样热搜里出现了“镜像安全 和容器安全”我把这两个也一并讲清楚。它们的关系恰好又回到镜像和容器的区别上一个是静态的一个是动态的。6.1 镜像安全从源头到静态文件逐层审查镜像安全关注的是**“这张图纸本身有没有问题”**。镜像一旦有漏洞、包含恶意软件、或者带着敏感信息那么基于它创建的所有容器都会遭殃。所以镜像安全的重心全在“源头和静态内容”尽量从官方仓库或可信源拉取镜像对拉下来的镜像做 hash 校验不要轻易使用来路不明的精简镜像某些第三方镜像会删掉安全组件甚至埋后门定期做漏洞扫描常见的工具有 Trivy、Clair 等镜像内不要写死口令、密钥、令牌改用环境变量或密钥管理服务注入遵循最小化原则基于瘦身基础镜像、不安装多余工具、减少攻击面。一句话总结镜像安全就是“你不会让陌生人拿着可疑图纸给你盖楼”。6.2 容器安全关注动态运行期的隔离和权限容器安全关注的是**“楼盖好之后住客在里面的行为”**核心是资源隔离和权限控制。Docker 容器之所以不像虚拟机一样拥有完整内核隔离是因为它靠的是 Linux 内核的 Namespace 和 Cgroups 两类技术Namespace 让容器看到各自的进程、网络、文件系统、用户空间Cgroups 限制每个容器能使用的 CPU、内存、IO 等资源。这就引出两个关键实践给容器加资源限制。不给限制的容器可以吃光宿主机全部 CPU 和内存影响其他容器甚至宿主机本身。启动时建议明确指定docker run -d --name myapp --cpus1.5 --memory512m myimage别小看这一步生产环境里没有资源限制的容器就是一颗不定时炸弹。尽量不用--privileged特权模式。特权模式等于把容器的权限边界直接拆了容器内进程可以访问宿主机的绝大多数设备。除非有明确且必要的原因否则我强烈不建议用。“容器资源隔离”这个词网上聊起来好像很玄实际落到操作层面就是 Namespace 加 Cgroups 的配合。理解这个底层再去看docker stats输出的 CPU/内存指标你会更清楚它背后的含义。7. 如果你刚开始学 Docker我的学习路线建议最后这部分不是总结是我带过不少人之后沉淀下来的学习顺序。你要是完全零基础按这个顺序走会比直接四处抄命令快得多也不容易陷入“镜像容器傻傻分不清”的泥潭。第一步先把“镜像只读、容器可写、一个镜像多个容器”这组关系背熟并用最原始的命令把基本操作走一遍docker pull、docker run、docker ps、docker exec、docker stop、docker rm。不要上来就写 Docker ComposeCompose 是让你同时管理多个容器的工具前提是你得先理解单个容器。第二步动手写第一份 Dockerfile。不用追求花哨就做一个能跑 Hello World 的小服务把FROM、RUN、COPY、CMD四个指令吃透。感受到“每一条指令生成一个层”这个过程镜像分层的概念就会真正刻进脑子里。第三步练习数据持久化。故意用一个没挂卷的容器写点文件然后删掉它亲眼看看数据是怎么丢的再挂上-v重来一对比差异。这个教训自己踩一次比看十篇教程都深刻。第四步试着用同一个镜像部署两个容器感受“共享镜像但互不干扰”是怎样的体验。这一步可以布置一个小作业用 nginx 镜像起两个容器分别映射到 8080 和 8081 端口各自挂载不同首页。第五步再进入 Docker Compose、网络模式、资源限制这些进阶话题。你会发现只要前面几步打下了镜像与容器关系的基础后面那些工具都只是“让这张图纸在更多楼之间高效协作”的锦上添花。最后说句个人经验我这些年排查过的 Docker 问题不管表象多复杂往深了挖八成都要回归到“镜像和容器到底谁是谁”这个最基础的关系上。把这一对关系刻在脑子里比记一百条命令都管用。