上周同事甩过来一张截图,docker images刷了三十多行,其中一半的 REPOSITORY 和 TAG 都是<none>,宿主机的/var/lib/docker已经吃掉 80G。他问了一个特别朴素的问题:这些玩意儿到底是啥,能不能直接删。我盯着那张图看了一会儿,发现真正难回答的不是"能不能删",而是他根本没搞清楚 Docker Image 是什么东西——在他脑子里,镜像约等于"一个安装包",删了就删了,大不了重新拉。这个认知偏差会一路蔓延到拉取、构建、推送、清理的每一个环节,最后以磁盘爆满或者no space left on device的形式爆炸。
这篇文章想干的事情,是把 Docker Image 这个概念和围绕它的一整套命令(pull、tag、push、build、rmi、prune、save、load等)从"会敲"推到"知道敲下去之后系统里发生了什么"。适合已经用过docker run、能看懂基本命令,但每次遇到<none>镜像、rmi删不掉、镜像体积莫名其妙膨胀时就只能靠搜索引擎的人。下面所有内容都是围绕实操展开的,每个参数我都会说清楚它动了系统中的哪一部分,而不是把--help抄一遍。
1. 从磁盘爆满说起:镜像其实是"一摞只读层"
先把最容易误解的地方钉死:镜像不是一个大压缩包,也不是一个虚拟磁盘文件。它是一组按照固定顺序排列的只读层,再配上一份描述这份顺序的配置清单。这个认知一旦建立起来,后面所有"为什么体积对不上""为什么删了之后空间没释放"的问题,都会变得顺理成章。
1.1 只读层叠加与可写层:容器启动时到底动了什么
我在本地拉一个mysql:8.0,然后docker run起来。这个过程中发生的事情大致是这样:镜像本身由若干层组成,比如基础的操作系统文件是一层,apt或者yum安装的依赖是一层,MySQL 的二进制包是一层,配置文件和环境变量是一层,最后是启动命令的元数据。这些层全是只读的,任何一层都不允许被修改。
容器启动时,Docker 会在最上面再叠一个可写层,容器里所有的写操作——写入数据文件、修改配置、生成日志——都只落在这一层上。底下的所有只读层被多个容器共享,这就是为什么你同时跑三个基于同一个node:20的容器,磁盘占用不会翻三倍。
这里有个新手非常容易踩的坑:容器里删掉一个文件,磁盘占用不但不会减少,反而可能增加。原因是底层只读层的内容不能被真正擦除,所谓"删除"只是在可写层里记了一条"这个路径被遮蔽了"的白障记录。所以在一个容器里跑完rm -rf /usr/share/doc/*,你用docker diff看会看到一堆变化,但镜像体积一点没动。
1.2 内容寻址:digest 为什么比 tag 更值得信任
每创建一层,Docker 会对这一层的内容做一次 SHA256 计算,得到的哈希值就是这一层的身份标识。这种设计叫内容寻址:名字(tag)可以变,内容哈希变了就是另一个东西。好处是不同镜像之间只要有一层的哈希相同,这一层就只需要存一份,跨镜像共享是天然发生的。
由此引出一个实操中的关键区别:nginx:latest这个 tag 指向什么,取决于仓库维护者最后一次推送的是什么;而nginx@sha256:xxxxxxxx...这个 digest 一旦写下来,就永远指向那一份确定的内容。生产环境里如果对可重现性有要求,用 digest 引用镜像比用 tag 稳妥得多。你可以用下面这条命令查到一个本地镜像对应的 digest:
docker image inspect nginx:latest --format '{{index .RepoDigests 0}}'注意:
RepoDigests只有在镜像从仓库拉取或者推送过之后才会有值。如果你是用docker build在本地构建出来的,没推过仓库,这个字段会是空的,这是正常现象。
1.3 为什么"改一行配置"会让镜像胖出几百兆
理解了层,就能理解这个现象。层的粒度是"指令",不是"文件"。Dockerfile 里一条RUN指令产生的所有文件变化,会被打包成一层。假设你写了这样两行:
RUN yum install -y gcc make && make build RUN rm -rf /tmp/build-cache第二行的rm只是加了一层"遮蔽记录",第一层里那几百兆的编译产物、临时文件、构建缓存,依然完整地躺在镜像里。最终镜像体积 = 所有层的总和,而不是最后那个文件系统的样子。
正确的做法是把会产生临时垃圾的操作,和清理操作放进同一条 RUN 指令,用&&串起来:
RUN yum install -y gcc make \ && make build \ && rm -rf /tmp/build-cache \ && yum remove -y gcc make这样中间产物在生产这一层的过程中就没了,不会进入最终镜像。这一个习惯能让一些 Java、Go 项目的镜像体积直接砍掉一半以上,属于性价比最高的优化手段之一。
2.docker images那张表,每一列都在说什么
很多人对docker images的输出是"扫一眼有没有我要的镜像"就完事了,其实这张表信息密度很高,尤其是当它开始出现异常数据的时候,每一列都是排查线索。
2.1 REPOSITORY 与 TAG:命名规则里藏着的坑
REPOSITORY这一列的完整形态其实是[仓库地址/][命名空间/]名称。当你不写仓库地址时,默认是 Docker Hub 的官方命名空间;写的时候必须符合小写字母、数字、.、_、-的规则,大写字母是绝对不合法的。
这一点在两种场景下特别容易出事。第一种是本地构建时给镜像起名叫MyApp:1.0,docker build -t MyApp:1.0 .会直接报错invalid reference format。第二种是从 Windows 或者 macOS 上复制的代码里带了大小写不一致的路径,构建脚本在 Linux 上跑的时候才暴露出来。我的习惯是:项目名统一小写加连字符,比如order-service,从源头上避免这个问题。
TAG这一列的默认值是latest,但请注意,latest在 Docker 里没有任何"最新版本"的语义,它只是"没写 tag 时用的那个默认标签"。很多人以为docker pull redis拿到的就是最新稳定版,实际上拿到的就是仓库维护者给latest打的那个 tag 所指向的版本,有时候它可能落后于某个具体版本号。生产环境我强烈建议锁死具体版本,比如redis:7.2.4-alpine,而不是redis:latest。
2.2 IMAGE ID 的由来,以及它和 digest 的关系
IMAGE ID这一列显示的哈希值,是对镜像配置文件(config JSON,里面记录了环境变量、启动命令、所有层的 diffID 列表等)做 SHA256 的结果,所以它和RepoDigest是两回事。前者标识"这份配置内容",后者标识"仓库里的那个 manifest"。
一个经常被问到的现象:为什么我本地docker images看到的 IMAGE ID,和同事那边看到的不一样?可能的原因有三类。一是架构不同(一个是 amd64,一个是 arm64),配置内容自然不同;二是构建时间不同导致某些时间戳字段不同;三是确实就是两个不同的镜像,只是 tag 撞名了。排查的时候不要靠肉眼比对 ID 前缀,用docker image inspect对比一下Created和Architecture两个字段更靠谱。
2.3<none>虚悬镜像的三种来历,以及要不要删
<none>镜像(官方叫 dangling image)是新手最迷惑的东西。它的产生路径主要有三条:
- 重新构建同名 tag 的镜像。旧的镜像失去了 tag 指向,就变成
<none>了。这是最常见的一种,比如你反复docker build -t my-app:dev .,每构建一次就多一个<none>。 docker pull覆盖了本地已有的同名 tag 镜像。老那份同样会变成虚悬。- 多阶段构建的中间层镜像。某些构建方式下中间产物会留在本地。
先确认它们到底是什么,再决定删不删:
# 只看虚悬镜像 docker images -f dangling=true # 看它们的创建时间和体积 docker images -f dangling=true --format "table {{.ID}}\t{{.CreatedSince}}\t{{.Size}}"判断逻辑很简单:如果创建时间是你最近一次构建的时间,八成就是被覆盖的旧版本,删掉没风险。如果创建时间很古老,或者体积异常大,建议先用docker image inspect看一眼它的Cmd和Env,搞不好是某个还在用的服务的历史版本。不确定就不要批量删,这是我在生产机器上踩过教训之后给自己定的规矩。
2.4 SIZE 列为什么和磁盘实际占用对不上
SIZE列显示的是这个镜像独占层的总和,共享层只算一次。所以你把同一台机器上所有镜像的 SIZE 加起来,会得到一个比du -sh /var/lib/docker大得多的数字,这是正常的,不是系统算错了。
反过来也有一种情况:docker system df显示的总量和du的结果也不一样。原因通常是du会把正在被引用但已经该回收的层也算进去,而docker system df只统计有引用的部分。想知道真实的、可回收的空间有多少,看这条命令的第二行:
docker system df -v提示:
docker system df -v会列出每个镜像、每个容器、每个数据卷的详细占用,输出很长但非常值得看一遍。我第一次认真看完之后,发现一个已经不用的 Elasticsearch 镜像吞了 6 个 G。
3. 拉取、推送与打标签:pull/push/tag的真实动作
docker pull大概是所有人学的第一条 Docker 命令,但它内部做的事远比"下载一个文件"复杂。理解这个过程,能帮你在拉取失败、拉取变慢、拉取到不对的架构时,迅速定位到是哪一环出了问题。
3.1docker pull背后的 manifest 协商与分层下载
一次完整的拉取大致分四步。第一步,客户端向仓库请求这个 tag 对应的manifest,这是一个 JSON 文档,里面列出了这个镜像由哪些层组成、每层的 digest 和体积、以及配置文件的 digest。第二步,客户端比对本地已经有哪些层(层的 digest 是内容哈希,可以精确比对),只下载缺的那部分。第三步,逐层下载并校验哈希,哈希对不上就丢弃重来。第四步,把配置文件和层组装成本地镜像,并给上 tag。
第二步是解释很多"玄学"现象的钥匙。为什么你换了一台机器拉同一个镜像,速度特别慢?因为本地一个层都没有,全部要下。为什么在同一台机器上拉node:20和node:20-slim,第二次感觉快一些?因为它们共享了部分基础层。为什么你会看到输出里有Pull complete和Already exists两种状态混在一起?Already exists的就是本地已经有的层。
多架构镜像也是在这一步暴露的。仓库里的 manifest 可能是一个manifest list,里面包含了 amd64、arm64 等多个平台的入口,客户端会根据自己当前的平台挑选对应的那一份。这就是为什么你在 M 系列芯片的 Mac 上拉到的镜像是 arm64 版本,而同事的笔记本上拉到的是 amd64。想强制指定平台:
docker pull --platform linux/amd64 mysql:8.03.2tag只是给镜像加了个别名,不复制任何数据
docker tag这个命令名字起得很误导人,它做的事情本质上是在镜像的引用表里增加一条记录,让一个新的名字指向同一份内容。不复制层,不占额外空间,速度是毫秒级的。
docker tag mysql:8.0 registry.example.com/infra/mysql:8.0-prod这条命令执行完之后,docker images里会多出一行,但两行的 IMAGE ID 完全相同,SIZE 也相同。真正让数据发生搬运的是下一步的push。
这里有个很实用的技巧:本地构建的镜像要推到私有仓库之前,必须先用tag把名字改成符合仓库规范的完整路径,因为docker push推的是 tag,不是 IMAGE ID。常见错误denied: requested access to the resource is denied,八成就是 tag 里的仓库地址和实际登录的仓库对不上。
3.3 推送到私有仓库的路径规范和常见拒绝原因
私有仓库的镜像命名是有严格规范的,格式是<仓库地址>/<项目名>/<镜像名>:<tag>。比如registry.example.com/infra/mysql:8.0。如果仓库本身有命名空间或者项目分层,这个路径必须完全匹配。
推送被拒的几类典型原因,按出现频率排:
| 报错信息 | 根本原因 | 处理方式 |
|---|---|---|
denied: requested access to the resource is denied | 未登录,或 tag 路径与仓库不匹配 | docker login后检查 tag 全路径 |
unauthorized: authentication required | 凭证过期或没有该项目的写权限 | 重新登录,确认账号权限 |
manifest unknown | 推送时目标 tag 已存在但被仓库策略锁定 | 换 tag,或确认仓库是否禁止覆盖 |
blob upload invalid | 网络中断导致分块上传失败 | 重试,检查代理和 MTU |
最后一条在跨机房推大镜像时特别常见,尤其是镜像超过 1G 的时候。我的一般做法是先把镜像推到就近的仓库,再由仓库之间做同步,而不是从开发机直推远端。
3.4 多架构镜像与--platform的使用时机
现在不少项目需要同时支持 amd64 和 arm64,甚至一些国产化环境下的非 x86 架构。这时候单靠docker build是不够的,需要用 buildx 做多平台构建:
docker buildx build \ --platform linux/amd64,linux/arm64 \ -t registry.example.com/app/order:1.0.0 \ --push .注意--push这个参数。多平台构建的产物无法直接 load 到本地 docker images 里,因为本地镜像库一次只能持有当前平台的那一份,所以它必须边构建边推送到仓库。如果你写--load就会报错,这是一个非常典型的卡点,我见过不止一个人在这儿折腾半小时。
4. 构建侧的镜像命令:build、commit、save、load 各自的适用面
前面聊的都是"围绕已有镜像"的操作,这一节聊"产生镜像"和"搬运镜像"。这两类命令经常被混用,比如有人用docker commit当日常构建手段,有人用docker save当备份方案,结果都会在某个时刻翻车。
4.1docker build的层缓存,是提速的关键也是踩坑的重灾区
docker build按 Dockerfile 的指令顺序执行,每条指令生成一层。如果某条指令的内容和上次构建时完全一致(包括指令本身、依赖的文件内容、以及上一层的结果),Docker 就直接复用缓存,不重新执行。
这个机制有个必须记住的特性:缓存是链式的,一旦某一层失配,它后面所有的层都会失效。所以 Dockerfile 里指令的顺序极其重要。把变化频率低的操作放前面,变化频率高的放后面。经典的反例是这样:
COPY . /app RUN npm install每次改一行业务代码,COPY层的缓存就失效,导致npm install要重跑一遍,白白浪费几分钟。正确的写法是先拷依赖清单再装依赖:
COPY package.json package-lock.json /app/ RUN npm ci COPY . /app.dockerignore也是这一环的重灾区。如果你的构建上下文里有node_modules、.git、target这些目录,它们在COPY . /app的时候会被全部塞进构建上下文发给 daemon,既慢又容易让缓存失效。一个基本的.dockerignore:
.git node_modules target *.log .idea .vscode4.2docker commit:什么时候它还值得一用
docker commit把容器的可写层提交成一个新镜像。官方文档里对它态度很冷淡,理由很充分:它产生的镜像没有构建历史、不可复现、体积控制全靠运气,别人拿到也搞不清楚里面改了什么。
但它有一个场景确实好用:临时排障。比如生产环境某个容器的配置被改坏了,你想先把它冻结成一个镜像保住现场,然后再慢慢分析,docker commit就是最快的选择。或者你在一个容器里手工调了半天把服务跑通了,想把这份状态先存下来当参考,也行。
docker commit -m "调通了连接池配置" -a "me" <容器ID> debug/pool-fixed:trial我的底线是:commit出来的镜像只允许存在于本地,打上trial、debug这类明显带临时色彩的 tag,绝不推仓库,也绝不用在生产部署链路上。
4.3save/load与export/import的四组对照
这两对命令看起来都是"导入导出",实际行为差别很大,搞混了会丢数据。
docker save把一个镜像的所有层、配置、标签打包成一个 tar 文件;docker load把它读回来,恢复成一个完整的、可以用docker run跑的镜像。这是镜像级别的搬运,用于离线环境或者没有仓库可用的场景。
docker export把容器的当前文件系统导出成一份扁平的 tar;docker import把它导入成一个只有一层的镜像。注意这里的关键差别:import出来的镜像没有历史层信息,也没有原来的环境变量和启动命令,你docker run的时候必须自己指定CMD。
| 对比维度 | save / load | export / import |
|---|---|---|
| 操作对象 | 镜像 | 容器 |
| 保留分层 | 完整保留 | 压成单层 |
| 保留构建历史 | 保留 | 丢失 |
| 保留 ENV / CMD | 保留 | 丢失 |
| 典型用途 | 离线传输镜像 | 抢救容器内文件、制作极简基础镜像 |
| 体积 | 通常较大 | 通常较小 |
我个人的使用频率大概是:save/load一年用几次,export基本只在需要把容器文件系统扒出来看的时候用。
4.4 一条完整的构建到推送流水线
把上面这些拼起来,一个不依赖 CI 系统、纯手工可跑的流程长这样:
# 1. 构建并打上本地 tag docker build -t order-service:1.0.0 . # 2. 给仓库路径打第二个 tag docker tag order-service:1.0.0 registry.example.com/app/order-service:1.0.0 # 3. 登录并推送 docker login registry.example.com docker push registry.example.com/app/order-service:1.0.0 # 4. 在目标机器上拉取 docker pull registry.example.com/app/order-service:1.0.0如果你是在 IDEA 里用插件打包镜像,本质上它执行的就是第 1 步,只不过把 Dockerfile 路径和 tag 配置化在界面上。插件的好处是帮你把 build context 缩小到了target目录,避免了把整个工程目录发给 daemon 的性能损耗;坏处是出问题时你看不到它到底怎么调的命令,所以我习惯在插件配置里勾上"显示构建命令",出问题的时候能对照排查。
5. 删除与瘦身:rmi和prune的雷区清单
这是最容易出事的一节。删镜像这个操作看起来简单,实际上要同时满足好几个条件才能真正删掉,而且删错了恢复成本很高(尤其是那些只在本地存在的、没推过仓库的镜像)。
5.1rmi删不掉的五种情况,逐一处理
docker rmi报image is being used by running container或者conflict的时候,先别急着加-f。加-f只是强制解除引用,很多时候它并不会真正释放空间,反而让你误以为删干净了。五种典型情况的处理方式:
- 有容器正在使用(包括
Exited状态的容器)。先用docker ps -a --filter ancestor=<镜像ID>找出是哪些容器,确认可以删之后docker rm掉容器,再删镜像。 - 镜像有多个 tag。
docker rmi <IMAGE ID>会报错,必须先按 tag 逐个删,或者用docker rmi <repo>:<tag>的形式一个个来。 - 镜像被其他镜像当作基础层引用。这种情况删不掉,也不该删,因为删了会影响上层镜像。真要删,得先处理上层。
- minikube、k3s 这类工具自己的镜像库。你在宿主机上执行
docker rmi是没用的,得先用eval $(minikube docker-env)切进去。 - Docker Desktop 环境下的路径错位。Windows 和 macOS 上的 Docker 跑在一层轻量虚拟机里,
/var/lib/docker是虚拟机内部的路径,你在宿主机上找不到它。空间回收要做的是在 Docker Desktop 设置里调整磁盘映像上限,或者执行清理命令让虚拟机内部释放。
5.2prune系列的语义边界,以及最危险的那个参数
prune系列有四个成员:docker image prune、docker container prune、docker volume prune、docker network prune,还有一个全家桶docker system prune。它们的共同点是删除"没有被引用"的对象,但"引用"的定义各不相同。
先说结论:docker image prune不带-a只删虚悬镜像,相对安全;带上-a会把所有"没有容器在用"的镜像全删掉,包括你精心打好 tag 的那几个。这两者的差别有多大,用一条命令就能看出来:
# 只删虚悬镜像,看看能回收多少 docker image prune --dry-run # 连没被容器引用的有 tag 镜像一起删 docker image prune -a --dry-run--dry-run是个好东西,先看一眼再决定。我自己的习惯是先在开发机上跑一遍 dry-run 看看数量,心里有数之后再执行。
docker volume prune是最需要警惕的。数据卷被删掉之后,里面的数据是真的没了,没有回收站。而且 Docker 判断"没被引用"的依据是"没有容器挂载它",一个停掉的容器不算引用。所以执行这条命令之前,一定要先docker ps -a把所有容器列出来,确认你没漏掉任何还需要的容器。
注意:
docker system prune -a --volumes这条命令会同时清掉镜像、容器、网络和数据卷。任何情况下都不要在生产机器上随手执行它,也不要把它写进任何"一键清理脚本"里。
5.3 一份可以定期跑的清理清单
给一个我自己在用的、有边界的清理套路,分三档,风险递增,每次只往上走一级:
# 第一档:清虚悬镜像和无用网络,风险最低 docker image prune -f docker network prune -f # 第二档:清所有已停止的容器,前提是你确认没有需要保留的现场 docker container prune -f # 第三档:清未被任何容器引用的镜像 docker image prune -a -f第三档执行之前,一定先跑docker images对着看一遍,把那些体积特别大又暂时用不到的基础镜像(比如postgres、elasticsearch)记下来,因为它们一旦被删,下次要用就得重新拉几百兆甚至上 G。在带宽紧张的环境里,重新拉一遍的时间成本远超你省下来的那点磁盘。
除了删,还有一条更值得做的瘦身路径:用更小的基础镜像。同一套 Java 应用,基础镜像从openjdk:17换成eclipse-temurin:17-jre-alpine,体积可能从 470M 掉到 180M 左右;如果用多阶段构建只留 JRE 和 jar 包,还能再压。这个改动的收益是长期的,比定期清理靠谱得多。
6. 报错排查与跨平台差异:从拉取超时到 Desktop 启动失败
最后这一节聊几个高频问题。它们的共同点是:搜索引擎上一搜一大把答案,但很多答案只给操作不给原因,照抄完之后下次遇到还是不会。
6.1 拉取慢、拉取中断的排查顺序
遇到docker pull卡住或者超时,按这个顺序排查效率最高:
先看是卡在哪一步。如果一直停在Pulling from library/xxx,说明是拿 manifest 的阶段出了问题,通常和网络到仓库的连通性有关;如果已经显示具体层在下载但速度极慢,那问题在带宽或者链路质量上,可以尝试换一个更近的镜像源。
再看是不是镜像本身太大。有些基础镜像例如某些带完整工具链的版本,单层就有好几个 G。这种情况下可以考虑先拉一个更小的版本(比如带-slim后缀的),跑通流程之后再换。
最后确认平台是否匹配。如果拉取过程中报no matching manifest for linux/arm64 in the manifest list entries,说明这个 tag 不支持你当前的架构,需要显式指定--platform,或者换一个支持多架构的镜像。
对于因为架构不匹配导致的运行时报错,还有个坑值得说:你用--platform linux/amd64在 arm64 机器上拉了一个 amd64 镜像,能拉下来也能跑起来,但它是靠模拟层跑的,性能可能只有原生的几分之一,CPU 密集型的任务会明显变慢。能找出原生支持的镜像,就别用模拟。
6.2 Windows 和 macOS 上的目录、路径与虚拟化问题
Docker Desktop 在这两个平台上的行为和 Linux 原生存明显差异,最典型的三点:
第一,没有/var/lib/docker这个路径。所有镜像和容器的数据都在一个磁盘映像文件里(Windows 下通常是ext4.vhdx,macOS 下是Docker.raw)。所以你在网上看到的那些"删掉/var/lib/docker/overlay2目录来释放空间"的教程,在这两个平台上完全不适用,照做只会浪费时间。
第二,文件挂载的性能差异。Windows 上从宿主机挂载目录进容器,在 WSL2 后端下如果跨文件系统访问,性能会明显下降。把工程代码放在 WSL2 的文件系统内部,而不是 Windows 的盘符下,构建速度的差别能有好几倍。
第三,启动时报虚拟化相关的错误。这类报错基本都指向同一个根因:底层的虚拟化能力没被正确启用。处理路径通常是先确认 CPU 的虚拟化选项在固件设置里是打开的,再检查系统里是否有其他虚拟化组件占用了同一套能力,产生冲突。这种情况下重启一次往往能让配置生效。如果你用的是较老的机器,还要确认它本身支持所需的虚拟化特性,这是硬性前提,绕不过去。
顺带提一个和镜像体积相关的实际场景。在资源受限的环境里用 MySQL 8.0,docker pull mysql:8.0拉下来的镜像本身就有几百兆,加上初始化数据目录之后磁盘占用会继续涨。我的做法是给数据目录单独挂一个数据卷,镜像本身用mysql:8.0而不是mysql:latest,避免某次重建容器时因为 tag 指向变化而被动升级到不兼容的版本。数据库这种组件,版本号必须显式锁死。
6.3 贴在备忘录里的命令速查表
把本文涉及的命令按用途归一下类,方便直接抄:
| 用途 | 命令 | 说明 |
|---|---|---|
| 查看本地镜像 | docker images | 加-a显示中间层,加-f dangling=true只看虚悬 |
| 查看详细元数据 | docker image inspect <名称> | 查 Created、Architecture、Env、Cmd |
| 带格式化输出 | docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}" | 自定义列,适合脚本解析 |
| 拉取并指定平台 | docker pull --platform linux/arm64 <名称> | 跨架构场景必用 |
| 打别名 | docker tag <源> <目标全路径> | 不复制数据 |
| 推送 | docker push <目标全路径> | 路径必须与仓库规范一致 |
| 构建 | docker build -t <名称> . | 加--no-cache强制不用缓存 |
| 多平台构建 | docker buildx build --platform ... --push . | 必须带--push或--load |
| 保存镜像为文件 | docker save -o img.tar <名称> | 含分层和历史 |
| 从文件载入 | docker load -i img.tar | 与 save 配对 |
| 导出容器文件系统 | docker export <容器ID> -o fs.tar | 扁平单层,无历史 |
| 删除镜像 | docker rmi <名称或ID> | 有容器引用时先删容器 |
| 清理虚悬镜像 | docker image prune -f | 相对安全 |
| 清理无用镜像 | docker image prune -a -f | 会删掉有 tag 但没被引用的镜像 |
| 查看空间占用 | docker system df -v | 明细最全 |
速查表这东西的价值不在于背下来,而在于遇到不熟悉的场景时能快速想起"有这么个命令存在"。我自己的习惯是每解决一个新问题,就往表里补一行,半年下来这张表就成了最贴合自己工作流的参考。
说回开头那位同事的问题。最后我是这么处理的:先用docker images -f dangling=true --format "table {{.ID}}\t{{.CreatedSince}}\t{{.Size}}"把虚悬镜像按创建时间排出来,确认最近三天构建产生的那些直接清掉,回收了大概 30 多个 G;剩余体积大的几个是他几个月前为了试某个中间件拉的,从没跑过容器,这种删掉之后下次要用重新拉就行。至于/var/lib/docker剩下那部分占用,是正在运行的几个数据库容器的数据卷,那个不能碰。整个过程最有价值的不是那几条命令,而是在动手之前花五分钟把"这个镜像是什么、谁在用、删了怎么恢复"这三件事想清楚。