☰
Docker镜像与容器核心操作:分层机制、数据持久化与排障命令详解
2026/10/6 3:46:35 网站建设 项目流程

1. 镜像操作前,先弄明白“镜像分层”这一底层机制

很多人学 Docker 到第二篇就卡住了:pull、run、exec、logs,命令能背,但真遇到“容器里改的文件去哪了”“镜像为什么删不掉”“mysql 一重启数据就没了”就彻底懵。原因很统一——镜像和容器这两个概念没想透。这篇既然是“镜像与容器核心操作命令”专题,我就不客气地默认你会装、会启动 Docker,直接拿这一章先把底层机制捋顺。

1.1 镜像不是“安装包”,容器也不是“解压结果”

最常见的说法是“镜像像模板,容器像实例”。这个类比不算错,但特别容易误导。你买一台新电脑,第一次装 Windows 就是“镜像刻录”,装完系统改了壁纸、装了软件,这台电脑的状态已经和镜像文件不一样了——这才是容器。

真正的差别在于:镜像只读,容器多了一层可写层。你在容器里写的任何文件、装的任何软件,都落在这一层薄薄的可写层上。一旦容器被删除(docker rm),这层东西就全部消失,而你 pull 下来的镜像本体依然原封不动地躺在磁盘里。

我把三者职责用表格拆开,后面所有命令都会围绕这张表展开:

对象是否可写生命周期典型用途被删除后数据去向
镜像只读拉取后长期存在模板、分发、版本管理删除镜像时清理
容器可写创建后运行/停止/删除跑起来的进程环境先删容器才能删镜像
数据卷可写独立于容器存在持久化数据、共享文件卷不随容器删除

这张表我建议你打印出来贴在工位。很多初学者把数据直接写进容器可写层,图了一时方便,后面容器一重建全部归零,那才是真正的“生产事故预习”。

1.2 一层一层叠出来的镜像,才是 pull 输出有“拉取层”的原因

镜像另一个关键设计是分层存储。你执行docker pull nginx:1.27-alpine时,看到的输出不是一整块文件下载,而是“Pull complete”一行接一行,每一行对应一个镜像层。

这背后是联合文件系统技术,各个镜像层被叠加成“看起来”是一个完整的文件系统。可这么设计图什么?两个理由:一是能复用,你本地如果已经有一个基础镜像层,再拉另一个基于相同基础层的镜像时,相同的层直接复用,不需要重新下载;二是能省磁盘,每个层都只保存“这个层相对上一层做了哪些改动”,而不是每拉一个新镜像就完整存一份整个文件系统。

所以我强烈建议你学会看docker history。比如想搞清楚nginx:1.27-alpine这个镜像到底由什么拼出来,直接执行:

docker history nginx:1.27-alpine

输出里会列出每一层的创建指令,从基础镜像alpine,到设置环境变量,再到复制配置、暴露端口、设定启动命令。这些层当初就是由 Dockerfile 里的一行行指令生成的。一条指令生成一层,镜像的“厚度”基本等于 Dockerfile 的行数密度。这也是我后面要讲“为什么不要写一长串 RUN 命令”的前置知识——层越多,打包和拉取越慢,不必要的中间层需要清理时才更麻烦。

搞懂分层后,你再回头看那句“容器是镜像的一个运行实例”会有新的理解:镜像是叠好的多层结构,容器只是在这堆层上方,额外叠了一个可写层,进程就跑在这个联合起来的文件系统里。

2. 镜像增删查改:管好本机镜像的四个核心命令

镜像相关命令看起来就那么几个,真正用得明白的人很少。主要因为大家默认把镜像当成“文件”处理,而没意识到镜像是有 tag、有 digest、有继承关系、还会产生悬空态的特殊对象。

2.1 pull 命令与 tag 机制:latest 标签背后藏着最大的坑

拉镜像的命令基本就一句:

docker pull nginx:1.27-alpine

注意这里nginx是镜像仓库名,1.27-alpine是 tag。nginx:latest当然也能拉,但我几乎从不在生产环境用 latest,原因很现实:latest 是个“移动标签”,镜像仓库维护者随时可以把 latest 指向新版本。你今天拉到的 latest 和明天拉到的 latest,可能是完全不同的东西;更麻烦的是,集群里不同节点在“不同时间点”拉了 latest,版本不一致的问题会直接变成一个说不清的故障。

如果你只记住 tag 还不够,更可靠的是记住 digest。每个镜像内容经过哈希计算会得到一个唯一的摘要值,表示内容和那份镜像一一对应。这样你每次拉取都确保是同一份内容:

docker pull nginx@sha256:2dc8c3e3c6cbe8d2394b5f6c5d5d3ec04b8f6b1f3d7e9c1d14a0b06c0a5e2b

digest 怎么查?用docker images --digests就能看到每个镜像的 digest。值得提醒的是,普通人日常开发用 tag 就好,但对“部署必须可重复、出问题必须可回溯”的场景,锁定 digest 是最稳妥的。这是从“能跑”走向“可靠部署”的一小步。

另外,关于拉取速度,国内网络环境下第一次拉镜像经常慢到怀疑人生,官方仓库的连通性也不稳定。我建议你在 Docker CLI 里配置 registry mirror,也就是镜像加速服务。配置路径通常是 Docker 设置里的 Docker Engine 配置项,加入类似下面的内容:

{ "registry-mirrors": [ "https://docker.mirrors.example.com" ] }

注意要用你所用云服务商提供的、合规的加速器地址。配置完重启 Docker,再拉镜像速度通常会有明显改善。

2.2 查镜像:images、inspect、history 各干各的活

docker images最常用,但也最容易扫一眼就关掉。它能告诉你本地有哪些镜像、tag 是什么、镜像 ID、以及占用磁盘大小:

docker images --digests

如果镜像特别多,可以过滤出只想看的:

docker images | grep nginx docker images --filter "dangling=true"

第二种过滤命令里的dangling=true,指的是悬空镜像。什么是悬空镜像?当你反复用同一个 tag 拉取新版本,旧镜像虽然内容还在,但已经没有 tag 指向它了,就变成<none>:<none>。这些镜像没人主动引用,却依然占着磁盘空间。所以看到一堆<none>不要慌,这是镜像更新过程中的正常产物,后面清理掉就行。

docker image inspect是拿镜像底层元数据的命令。你想知道镜像的架构、操作系统、暴露端口、环境变量、入口命令,它可以一次性全给出来。排查问题的时候特别有用,比如容器起不来,你想确认镜像里的CMD到底是什么,直接看:

docker image inspect nginx:1.27-alpine

输出是一个长 JSON,建议配合jq用:

docker image inspect nginx:1.27-alpine | jq '.[0].Config.Cmd'

docker history则用来追溯镜像层的来源。我前面说过每一个镜像层对应构建时的一条指令,比如你拿到第三方镜像想知道它里面加了什么,history能非常直观地告诉你:更新依赖、装软件、改配置、设置启动命令,一层层像“考古”一样展开。这也是判断“这个镜像靠不靠谱”的快速手段。一个镜像 history 里全是乱七八糟的RUN拼接,哪怕它在仓库里下载量很高,你也要保持警惕。

2.3 删镜像:rmi 与 image prune 的正确组合

删除镜像的命令很简单:

docker rmi nginx:1.27-alpine

但实际删除时最常遇到的报错是:

Error response from daemon: conflict: unable to remove repository reference "nginx:1.27-alpine" (must force) - container

意思是已有容器在引用这个镜像,必须先删容器再删镜像。我不建议你直接加-f强制删除,那样容易把正在运行的容器底层镜像文件删掉造成更诡异的问题。正确做法是先看有哪些容器依赖这个镜像:

docker ps -a | grep nginx

把相关容器停止并删除后,再执行rmi。

本地镜像攒多了之后,单条删除太麻烦,用批量清理命令:

docker image prune

默认会清掉所有悬空镜像,而保留有 tag 引用的镜像。想更激进一点,把没有被容器使用的镜像全部干掉的:

docker image prune -a

但注意-a会把“已经不存在容器引用”的镜像一口气清理掉。如果你有个镜像是特意留着备用但暂时没用,就别用这个命令。

如果你只想清理某段时间之前产生的悬空镜像,加个 filter:

docker image prune --filter "until=168h"

这里168h表示七天内没有被引用且已悬空超过七天的镜像。这套组合下来,磁盘基本告别“莫名满了”的状态。

3. 容器从创建到删除的全生命周期操作细节

镜像只是原料,真正干活的容器才是主角。一个容器从诞生到退休要经历好几个状态,命令也远比“run 一下”多得多。这个生命周期掌握不了,后面排障、清理、部署都会处处踩坑。

3.1 run 实际上是 create、start、attach 三步组合

很多人以为docker run nginx:1.27-alpine是个单一操作,其实它背后是三步:docker create创建容器、docker start启动容器、必要时再attach到容器终端。官方拆开的目的就是给你更细的控粒度:想创建配置好但先不启动,可以用 create;已经创建的容器,等时机到了再启动。

所以run本质上是个“组合命令”。我们先看最常见的完整形态:

docker run -d --name web-server -p 8080:80 --restart unless-stopped nginx:1.27-alpine

拆开解释:

  • -d:后台运行,不加这个你会在前台不断看到 nginx 访问日志,终端一关容器就跟着退。
  • --name web-server:给容器起名字。不加的话 Docker 会随机给一个“prickly_benz”之类的滑稽名字,排查时很难受。
  • -p 8080:80:把宿主机 8080 端口映射到容器内 80 端口。注意容器内有自己的网络隔离,不映射的话宿主机访问不到容器里的 nginx。
  • --restart unless-stopped:容器退出时自动重启,除非是你手动停的。这是生产环境非常常用的重启策略。

还有一个初学者必踩的坑:交互式命令要用-it。比如想临时跑一个 alpine 容器进去敲命令:

docker run -it alpine sh

-i保持标准输入打开,-t分配一个伪终端。两个参数必须成对出现,少了-t你会发现 shell 没有交互提示符,少了-i你敲命令输入不进去。一定要先记住“交互就 -it,后台就 -d”,这能省掉不少时间。

3.2 stop、kill、pause、restart:停止容器有不同的力度

容器运行期间,你常见的操作对象可以从docker ps看到。但要小心,docker ps默认只会列出状态为 Up 的容器,加了-a才能看到那些已经退出和刚创建但没启动的容器。

docker ps -a

输出里STATUS一列很关键,有Up、Exited、Created、Paused、Restarting等状态。对状态的拿捏决定了你用哪条停止命令:

命令实际行为结束信号适用场景
docker stop优雅停止先 SIGTERM,宽限期后 SIGKILL正常下线服务
docker kill立即停止直接 SIGKILL进程卡死、无响应
docker pause冻结进程暂停容器内所有进程临时挂起,不结束容器
docker restart重启容器先 stop 再 start应用状态异常时尝试恢复

举个直白的例子,docker stop相当于你给进程发了“请交接完手头工作再下班”的通知,处理完信号后才会退出;docker kill相当于“马上断电”,进程连清理机会都没有。生产环境里能用 stop 就别用 kill,尤其数据库这类需要刷盘保存状态的服务,直接 kill 很容易坏数据。

docker pause平时容易被忽略,但在做备份或排查时非常有用。比如你要对容器内文件打一个一致性快照,直接执行docker pause mysql-container,容器进程全部冻结住,文件不再变化,打完快照再docker unpause mysql-container恢复。不比 stop/start 优雅得多。

3.3 删除容器:rm、prune 以及批量清理

想删容器,要分两步:先确认已经停止,再执行:

docker rm web-server

如果容器还在运行,直接删会报错,提示你容器未停止。这时候多一个-f可以强制停止并删除,不过我同样建议少用——强制删除会把 stop 该做的 SIGTERM 省掉,对数据库之类服务不友好。

一个实用技巧是删容器和停容器连在一起:

docker rm -f $(docker ps -aq)

ps -aq拿到所有容器 ID(包括停止的),rm -f强删。这是开发环境清理专用,生产环境慎用。

当你有一堆已经停止的僵尸容器躺在那儿时,最省事的:

docker container prune

它会问你是否确认删除所有停止状态的容器。加上-f跳过确认。这里有个细节值得说明:prune 默认只清除停止的容器,运行中的容器不会被误杀,所以相对安全。

4. 进容器排障与日志定位:线上问题不再靠重启解决

容器最让人头疼的时刻,莫过于启动后“秒退”或者服务假死。初学者第一反应是删掉重建,但成熟的排障思路应该是:先进容器的世界看进程、看日志、看资源占用。这一节我分享的正是日常调试工具集。

4.1 exec 与 attach 两种“进容器”方式差距很大

现在有一个正在后台运行的容器,我想进去查看或者操作它,大多数人第一句就是docker exec:

docker exec -it web-server sh

这条命令的含义是:在正在运行的容器web-server里,额外启动一个新进程sh,并把这个进程绑定到当前终端。因为它是“额外”启动的进程,所以你在里面敲什么命令都不会影响容器的主进程,退出这个 shell 也不会导致容器停止。

docker attach则完全不同,它不是开新进程,而是把当前终端“贴”到容器主进程的标准输入、输出和错误流上。你执行的docker attach web-server就像直接端坐在这个容器的主进程面前,看到的是主进程打出的日志,按Ctrl+C发送给主进程的 SIGINT 信号,有可能直接终止容器主进程,容器立刻退出。

所以日常排障,我只会用exec。attach这种“绑定主进程”的用法更适合在主进程本身有交互式终端、需要手动操作它的场景里。你如果分不清,就记住一句话:exec 是“进去新开一个 shell”,attach 是“连上正在运行的主进程”。

4.2 “启动即退出”排查链路:从 logs 找 root cause

最典型的问题场景:你docker run了一个 mysql 容器,结果容器刚启动就变成 Exited(1)。此时很多人立刻删掉容器重新 run,同一条命令跑十遍结果还是失败。真正高效的排查链路是这么走的:

第一步,先看容器当前状态:

docker ps -a

确认容器确实没起来,拿到容器 ID 或名字。

第二步,看日志:

docker logs mysql-container

这一步八成能直接给你答案。比如 mysql 启动失败的日志里通常会出现“Can't start server: Bind on TCP/IP port... Permission denied”或者“Initialize different directory”,这些信息能快速定位是端口冲突还是数据目录权限不对。我见过太多人因为 mysql 失败就反复重装,最后用docker logs看一眼,发现不过是宿主机 3306 端口被占了。

第三步,如果日志提示了但问题不好判断,才考虑进容器或查资源。比如它提示内存不足,那就跑一句docker stats去看整个主机的容器内存占用情况。

这里要特别强调一个基本功:容器内没有 systemd。你没法用systemctl status那一套去看服务为什么没启动,日志机制也完全不同。容器里的主进程就是“前台进程”,日志全部走标准输出,docker logs是唯一的日志入口。这个思维转过来,容器排障效率立刻上一个台阶。

4.3 实时状态监控:stats、top、events 怎么搭

容器运行起来之后,想看它是否健康、资源占用多少,我有三件套推荐。

docker logs加参数可以跟踪输出:

docker logs -f --tail 200 web-server

-f表示持续输出,--tail指从头文件末尾开始算多少条。开发阶段调试 nginx 或应用,这个命令我把当“顶替 tail -f”用。

docker top查看容器内实际运行的进程:

docker top web-server

它展示的是宿主机视角下属于这个容器 PID 命名空间内的进程列表。比如 nginx 容器里到底起了一个 master 进程还是几个 worker,这里一目了然,也能快速发现容器里是不是被塞进了额外的进程。

docker stats是资源监控利器:

docker stats

输出是实时刷新的表格,显示每个容器的 CPU 百分比、内存占用和限制、网络 IO、磁盘 IO。没有它,你很难判断某个容器的内存为什么会持续涨。

docker events则是看容器生命周期的“事件流”:

docker events --filter container=web-server

容器创建、启动、停止、销毁、被 OOM 杀掉,都会在这里留下一条事件记录。我之前排查过一台机器上容器频繁消失的问题,单纯看ps和logs都看不出结构,最后是docker events里连续几行 OOM kill 事件把真凶暴露了——内存限制设得太小,容器运行几分钟就被系统杀掉。

5. 容器资源限制与数据持久化:部署真正可用服务的分水岭

前四节的命令,能让你把容器“跑起来”并且“排好障”。但真实部署一个能用很久的服务,还差两块硬骨头:资源隔离的边界、数据持久化的策略。这也是网上大量“容器跑一天就挂”“容器 rm 之后数据全没”的根源。

5.1 资源限制:内存、CPU 及容器资源隔离的边界

“容器资源隔离”不是一句口号,而是靠内核能力实现的,体现在 Docker 参数上就是内存和 CPU 的限制。一个完全没做资源限制的容器,理论上可以吃光宿主机的全部内存和 CPU,甚至拖垮宿主机上的其他服务。这不是危言耸听,线上真见过跑了个 Java 容器把整台机器吃爆的。

给容器限制内存:

docker run -d --name app -m 512m --memory-swap 1g nginx:1.27-alpine

-m 512m设定内存上限 512 MB,--memory-swap 1g设定内存加交换分区总共 1 GB。限制 CPU:

docker run -d --name app --cpus 1.5 nginx:1.27-alpine

表示容器最多使用 1.5 个 CPU 核。也可以精确配额:

docker run -d --name app --cpu-shares 1024 nginx:1.27-alpine

--cpu-shares默认值是 1024,表示相对权重,数值越大在这个宿主机上抢占 CPU 的优先级越高。多容器共处一台机器时,这个参数比--cpus更灵活。

设置限制之后,docker stats能看到限制是否生效。内存限制有一个副作用要提前知道:容器内存超过限制时,系统可能直接给容器发 OOM kill,容器就这样“莫名”退出。

5.2 持久化:容器可以被随时销毁,但数据不行

容器这个概念的天然属性就是“可以被随时销毁重建”。如果你把数据写在容器可写层里,容器删了,数据跟着没了。生产环境的 mysql、redis、业务文件,绝不能依赖容器可写层。

两种主流持久化方式:bind mount 和 volume。bind mount 直接将宿主机某个目录映射进容器:

docker run -d --name mysql-server -v /my/own/datadir:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=123456 mysql:8.0

/my/own/datadir是宿主机目录,/var/lib/mysql是容器内 mysql 的数据目录。目录映射的好处是你能直接用宿主机文件工具管理,但坏处也很明显:宿主机目录的权限和路径需要你完全把控,跨机器迁移时配置里的路径到处要改。

我日常更推荐 volume,它由 Docker 管理,不依赖具体路径:

docker volume create mysql-data docker run -d --name mysql-server -v mysql-data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=123456 mysql:8.0

volume 的优势在于:位置由 Docker 统一管理,备份和迁移有专门命令,权限处理更安全,而且它独立于容器生命周期——删掉容器、重建容器,挂到同一个 volume 上数据照样在。

这里统计一下刚踩过的典型坑:很多人为了让“数据持久化”,把宿主机的一个普通目录映射进容器,却忽略了目录权限。比如上面 mysql 容器,宿主机目录如果权限是 755 且属主不是 mysql 对应的 uid,容器内 mysqld 就可能没有写入权限,直接启动失败。遇到这个情况先别想着重装,看一下docker logs里的权限报错,处理宿主目录权限即可。

5.3 docker cp 只能救急,不能当网盘用

还有一个高频命令docker cp,用来在宿主机和容器之间拷贝文件:

docker cp ./backup.sql mysql-server:/tmp/backup.sql

把容器里的文件拷到宿主机:

docker cp mysql-server:/var/log/mysql/error.log ./error.log

它的使用前提是“容器还在”。我建议只把它当救急工具,比如临时调个配置、拉出一个日志,不应作为日常数据交换方案。日常数据的交换和持久化,一定要用 volume 或者 bind mount 提前设计好。容器一旦删除或重建,docker cp 这条路立即失效,而 volume 从来不会因此受影响。

还有一个小提醒:docker cp不支持跨宿主机拷贝,也不支持同步,它只是单个文件或目录的“一次性搬运”。想让容器和数据文件在机器之间流转,正确的思路是构建镜像、推送镜像、再在新机器上拉取运行,数据则用 volume 或专门的存储方案解决。把 cp 当网盘用,早晚会在数据同步上吃亏。


说实话,这套内容我自己也是踩了无数坑才理顺的。最初我也习惯“容器坏了就删掉重开”,后来被 mysql 数据清空狠狠教育过,才开始认真研究镜像分层和 volume 持久化。现在我把最常用的经验总结成一句话送给你:镜像负责定义“怎么运行”,容器负责承担“当前运行”,数据卷负责留住“不能丢的东西”,日志则负责告诉你“到底发生了什么”。这四个角色分清楚了,Docker 的核心操作命令自然会各就各位。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询