Docker镜像管理实战:深入掌握docker images命令与清理技巧

Docker镜像管理实战:深入掌握docker images命令与清理技巧 1. 先搞明白docker images到底在列什么1.1 顶层镜像、中间层镜像和“隐藏镜像”我印象很深第一次把 Docker 装好敲下docker images看到一长串列表以为这就是本机全部镜像了。后来在 CI 机器上排查磁盘占用才发现这个判断完全错了。docker images默认只列出“顶层镜像”而真正躺在本机占用磁盘的还有一大堆中间层镜像它们默认不出现只有加了-a才会显示。这里涉及 Docker 镜像的基本结构一个镜像不是一个大文件而是由多层只读层堆叠出来的。Dockerfile 里的RUN、COPY、ADD等指令基本都会生成一层构建过程中会产生很多中间层镜像。这些中间层最大的价值是缓存下次构建时只要某一层没变就能直接复用。但它们对我们日常使用来说没有太大意义所以默认列表把它们隐藏了。真正会出现在默认列表里的是那些没有被其他镜像引用的“顶层镜像”以及已经打了标签的镜像。如果你在构建过程中留下了一些没有被打标签的镜像也就是none:none那种行它们也可能会出现在默认列表里这类镜像就是我们常说的“悬空镜像dangling image”。悬空镜像和中间层镜像很多人分不清简单说悬空镜像是没有标签、也没有被任何其他镜像引用的镜像可以删中间层镜像虽然也大多是none标签但它们还被别的镜像引用着强行删会被拒绝。所以以后看到docker images -a输出里出现大量none不要慌也别急着全部删掉。先用docker images --filter danglingtrue筛出真正可以清理的悬空镜像再决定下一步。1.2 默认输出那几列每一列到底什么意思docker images默认输出五列很多人扫一眼就过去了其实每列都有细节REPOSITORY镜像的仓库名可能是官方仓库上的nginx也可能是私有仓库地址加路径比如registry.example.com/app/backend。TAG标签比如latest、1.25。没被打标签的镜像这里会显示none。IMAGE ID镜像 ID默认显示 12 位短 ID真实 ID 是 64 位十六进制字符串。短 ID 和长 ID 都指向同一个镜像短 ID 只是给人类看的缩写。CREATED镜像创建时间不是拉取时间也不是最后一次使用时间。很多人在脚本里想“找出最近一周用过的镜像”用这一列是不行的。SIZE该镜像本身占用的磁盘大小注意 Docker 会共享层所以两个镜像 SIZE 加起来不一定等于它们实际多占的磁盘空间。这个理解直接影响你怎么做清理。比如nginx:1.23和nginx:1.25可能共用一部分基础层但它们是两个不同的镜像。反过来同一个 IMAGE ID 出现在两行里说明这其实是同一个镜像只是多打了一个标签而已。此时你用docker rmi删其中一个标签并不会删除镜像本身只有最后一个标签被移除时镜像数据才可能真正被释放。1.3docker images和docker image ls是什么关系Docker 在 1.13 版本之后开始推行子命令体系把很多根命令慢慢往docker image下面收。docker image ls就是新体系里的正式命令docker images是历史遗留写法。两者目前是等价的参数也完全通用。我自己在交互式命令行里还是习惯敲docker images因为顺手、少敲两个字。但在写脚本的时候更推荐用docker image ls语义更清晰别人接手你的脚本时也更容易看懂这是在查“镜像列表”而不是什么别的操作。如果你在一个老版本 Docker 环境里工作docker images的兼容性更好但只要不是太老的版本两者都可以放心用。2. 常规参数逐个拆从-a到--digests各自解决什么问题2.1-a/--all把中间层镜像也摆上台面-a的全称是--all作用是显示所有镜像包括中间层镜像。这是我第一个建议你养成的习惯排查问题时先跑一次docker images -a看看全量镜像到底有多少别只看默认输出。有一个实际场景特别典型某天我负责的构建服务器磁盘满了我跑docker images看列出来的镜像加起来也不到 10GB但df -h显示磁盘已经用了 60GB。原因就是构建过程中残留了大量中间层镜像和悬空镜像默认列表根本看不到。加了-a之后几十个none镜像全部出来了这才定位到问题。要注意的是-a只是“显示”不等于“可以全删”。中间层镜像被引用的状态不会因为没有标签而解除直接docker rmi会被拒绝。真正应该删除的悬空镜像用--filter danglingtrue筛选更精准。2.2-q/--quiet给脚本用的极简输出-q只输出 IMAGE ID不输出表头和其他列每行一个 ID。这个参数在脚本里非常有用因为我们可以把 ID 直接喂给docker rmi、docker tag或者其他命令。比如docker images -q docker images -a -q docker images --filter danglingtrue -q第一个命令列出所有顶层镜像的短 ID第二个命令把中间层也带上第三个命令专门输出悬空镜像 ID。注意--quiet模式下输出的还是短 ID如果你需要完整 ID要配合--no-trunc。有个坑必须提前说docker images -q在没有任何镜像时会输出空内容这时候如果直接拿去执行docker rmi $(docker images -q)docker rmi会因为没有任何参数而报错。后面我会专门讲怎么避免这个坑。2.3--no-trunc截断的 ID 有时候会误事默认 IMAGE ID 只显示 12 位这已经足够区分绝大多数场景了。但在某些情况下必须看到完整 64 位 ID比如在内网笔记或文档里记录某个镜像的精确版本多个镜像短 ID 前 12 位碰巧相同概率很低但不是没有排查镜像层引用关系时需要拿完整 ID 去和其他命令的输出做比对。--no-trunc可以和多种参数组合使用。我最常用的组合是docker images --no-trunc --format {{.ID}} {{.Repository}}:{{.Tag}}把完整 ID 和仓库标签一起打出来既保留可读性又能拿到完整信息。2.4--digests摘要这一列要不要看--digests会在输出中增加一列DIGEST也就是镜像清单manifest的摘要格式类似sha256:36b8...。这列信息主要和镜像仓库相关如果你只在自己机器上构建镜像本地构建出来的镜像通常显示none不用觉得奇怪。什么时候值得关心 digest当你在做多架构镜像、或者需要精确验证“某个 tag 对应的远端镜像有没有被动过”时。因为同一个 tag 可以被重新推送tag 没变但 digest 变了。比如团队 CI 拉取镜像如果只锁 tag下一次拉到的可能就不是之前那个内容如果锁 digest就能保证拉取内容完全一致。日常开发阶段--digests用得不算多但在排查“为什么代码没更新”这类问题时看一两次 digest 往往能直接发现 tag 没变、镜像内容变了的真相。为了让你对参数有个整体印象我把常用参数列成一张速查表参数作用适合场景-a/--all显示包括中间层在内的所有镜像磁盘排查、构建缓存分析-q/--quiet只输出镜像 ID脚本自动化、批量操作--no-trunc不截断 ID记录精确 ID、深度排查--digests输出镜像摘要多架构、镜像溯源-f/--filter按条件过滤悬空镜像、时间范围、仓库匹配--format自定义输出模板报表、脚本、JSON 转换3.--format才是真正的“自定义视图引擎”3.1 Go template 占位符速查--format是docker images里上限最高的参数它用 Go template 语法控制输出内容。简单说你能决定每一行输出哪几个字段、以什么顺序、用什么分隔符。常用占位符有这些{{.ID}}镜像 ID{{.Repository}}仓库名{{.Tag}}标签{{.Digest}}摘要需要--digests配合才更有意义{{.Size}}大小默认人类可读格式比如123MB{{.CreatedSince}}相对创建时间比如2 weeks ago{{.CreatedAt}}绝对创建时间最简单的玩法docker images --format {{.ID}} {{.Repository}}:{{.Tag}}这个输出就把默认表格变成了“ID 仓库名 标签”的紧凑列表。我在写巡检脚本时经常用这个格式因为默认表格的表头在管道处理时会是多余的干扰信息。3.2 用table关键字保住表头如果你想把输出变成自定义列但又不想丢掉表头可以在模板前面加table关键字docker images --format table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.Size}}这样输出会保留一行表头后面的行按你指定的列输出。这个格式特别适合生成报告或者直接重定向到文本文件里发给同事看。有一点是经验之谈模板字符串外面的引号很容易被忽略。如果你只想输出一个字段单引号双引号区别不大但如果模板里有\t这种转义序列或者在 shell 里还要做进一步展开建议统一用双引号并且在写进脚本前先手动跑一遍确认输出格式符合预期。不同 shell 对转义字符的处理方式不完全一样眼看为实。3.3 把输出变成 JSON脚本化处理的正路--format还有一个非常实用的技巧输出 JSON。docker images --format {{json .}}这个命令会把每个镜像的信息按 JSON 对象输出每行一个。光是这个能力就能省掉不少手工解析表格的麻烦。你可以直接接jq处理docker images --format {{json .}} | jq -r [.Repository, .Tag, .Size] | tsv也可以把输出重定向到文件交给 Python 或 Go 脚本做后续分析。我自己的经验是如果需要做二次处理优先用{{json .}}而不是写一串复杂的文本解析逻辑。默认表格是人看的JSON 是程序读的强行从人看的格式里做字段截取往往后面还要踩编码、空格、制表符的坑。4.--filter的实战价值悬空镜像、时间范围与引用匹配4.1 用danglingtrue精准找出悬空镜像绝大多数人第一次用--filter是冲着悬空镜像去的docker images --filter danglingtrue这个命令列出的就是REPOSITORY和TAG都是none的镜像。它们一般是构建历史遗留的旧版本或者重新打标签后残留下来的镜像。这类镜像最值得优先清理因为大概率已经没有业务在用了。但要注意即使danglingtrue筛出来的镜像也可能正在被某个容器使用。比如某个容器当时就是用这个镜像 ID 启动的只是镜像标签后来被移除了。所以执行删除前最好先确认没有容器依赖这些镜像。Docker 在删除正在被容器使用的镜像时会报错这其实是保护机制不要习惯性加-f强行删。4.2before和since按创建时间范围过滤--filter beforenginx:1.25可以列出所有创建时间早于nginx:1.25的镜像--filter sincenginx:1.25则是列出晚于它的镜像。注意这里比较的是镜像的创建时间不是构建依赖关系也不是拉取时间。我一般用它做“版本发布后的验证”docker images --filter sincerelease-2024-11-01如果某个镜像的日期明显早于这个版本说明它可能是上次发布之前就存在的老镜像需要进一步确认是否还在使用。这个参数也能用来粗筛旧镜像但由于同一镜像仓库可能包含大量历史 tag直接拿这个结果做批量删除风险不小建议只把它当筛选工具配合人工判断再执行清理。4.3reference按仓库名和标签匹配reference过滤器是我日常用得最多的。它支持通配符匹配仓库名和标签docker images --filter referencenginx:* docker images --filter referencemyapp/* docker images --filter reference*:latest第一个命令只显示nginx仓库下所有 tag第二个命令显示myapp/前缀下所有镜像第三个命令只看所有仓库里 tag 为latest的镜像。这里必须提醒一个很容易踩的雷shell 会把*当作通配符展开如果你不把 filter 条件用引号包起来它可能被展开成当前目录下的文件名导致命令输出完全不是你想要的结果。# 错误写法 docker images --filter referencenginx:* # 正确写法 docker images --filter referencenginx:*4.4label过滤器它筛的是元数据不是 TAG有些刚接触 Docker 的人看到label这个名字以为它可以按标签过滤比如“把所有latest标签的镜像筛出来”。这是个很常见的误会。label过滤的是镜像的 LABEL 元数据也就是 Dockerfile 里通过LABEL指令写入的键值对跟列表里的 TAG 列完全是两码事。Dockerfile 示例LABEL maintaineropsexample.com LABEL version1.2.3对应查询docker images --filter labelmaintaineropsexample.com docker images --filter labelversion1.2.3如果想要按 TAG 列筛选用的是上一节说的reference不是label。这两个名称相似但功能不同一定要区分清楚。多个--filter可以叠加叠加后的语义是“与”也就是必须满足所有条件才会显示。比如docker images --filter danglingtrue --filter labelkeepfalse这个命令只会显示既处于悬空状态、又带有keepfalse标签的镜像。理解这个“与”的关系能帮你做更精细的批量清理而不是动不动就全量删。5. 组合起来用清理磁盘和例行巡检的高频命令5.1 安全地清理悬空镜像清理悬空镜像最直接的方式是docker image prune -f这条命令把悬空镜像清理掉不需要手动查 ID。但如果你还是想用docker images自己组合脚本可以这么写ids$(docker images --filter danglingtrue -q) if [ -n $ids ]; then docker rmi $ids fi先判断变量是否为空再执行删除这样就避免了“没有镜像可删时 docker rmi 因为没有参数而报错”的尴尬。如果你的环境里xargs支持-r也可以写成docker images --filter danglingtrue -q | xargs -r docker rmi-r表示输入为空时不执行后面的命令。不过 macOS 自带的 BSD xargs 不一定支持-r跨平台脚本建议还是用上面的if写法。5.2 按仓库批量清理历史版本我想清理myapp仓库下除latest之外的所有历史版本可以用docker images --filter referencemyapp/* --format {{.Repository}}:{{.Tag}} | grep -v :latest | xargs -r docker rmi这条命令先按仓库名筛出myapp/前缀的镜像然后通过grep -v :latest去掉最新版本再把剩下的 tag 交给docker rmi。执行之前我建议先不要直接接docker rmi而是先跑一下前面部分把即将删掉的清单打出来看一眼docker images --filter referencemyapp/* --format {{.Repository}}:{{.Tag}} | grep -v :latest确认清单没问题再补上后面的管道。这个“先看后删”的习惯能帮你避开很多因为 tag 拼错导致的误删。5.3 快速生成镜像清单如果要定期把机器上的镜像清单发给团队核对我常用这个命令docker images --format table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.Size}}\t{{.CreatedSince}}它生成的表格比默认多保留了一点自定义空间同时表头还在直接输出到文件后可以在线预览或者发到群里。如果团队有自动化平台想要更结构化的数据就换成 JSONdocker images --format {{json .}} images.json两种格式对应两种场景给人看的报告用表格给程序用的数据用 JSON。不要混着用否则后面解析成本会很高。这里还必须提一嘴docker system df。查磁盘占用时它比docker images更像全局视角会显示镜像、容器、数据卷、构建缓存各自的占用大小。我在做例行巡检时习惯把两者结合先用docker system df看总体再用docker images配合 filter 看具体是哪个仓库占了大头。5.4 常用组合速查目标命令片段只看所有镜像 IDdocker images -q查看全量镜像含中间层docker images -a只看悬空镜像docker images --filter danglingtrue按仓库名筛选docker images --filter referencemyapp/*按时间范围筛选docker images --filter sincenginx:1.25自定义列表字段docker images --format table {{.Repository}}\t{{.Tag}}\t{{.Size}}输出 JSONdocker images --format {{json .}}6. 我踩过的几个坑和最后几条实操建议6.1 看到-a里大量none镜像不要慌第一次加-a的时候我被输出里密密麻麻的none行吓了一跳下意识想全部删掉。后来才明白这些里面一大部分是中间层镜像删也删不掉硬删只会报错。真正判断能不能删除要看它是否被引用。docker images --filter danglingtrue筛出来的悬空镜像在没有容器在使用的前提下删起来相对安全而-a列表里那些被其他镜像依赖的层哪怕显示none也不能轻易动。更靠谱的方案是用docker image prune让 Docker 自己判断哪些能删而不是拿docker images -a的输出当删除清单。如果你只是想看磁盘占用不要用docker images -a手动加总直接docker system df它会列出更准确的占用情况这也是官方更推荐的入口。6.2 引号问题大部分诡异行为都出在这里docker images的 filter 和 format 参数看起来简单但引号不对就会出现特别隐晦的问题。常见错误是 filter 值里的*没加引号被 shell 展开了。比如你在一个目录里有nginx-dev、nginx-prod两个文件执行docker images --filter referencenginx*shell 会帮你把nginx*展开成文件列表然后 Docker 接到的参数就完全变了。这种问题不会稳定复现只在特定目录下出问题排查起来很头疼。我的原则是只要 filter 或 format 里出现*、?、空格、反斜杠一律加双引号。别相信自己能记得住统一加引号成本最低。6.3 批量删除时别忽略“空输入”和“多标签”两个隐藏条件脚本里执行docker rmi $(docker images -q)时如果系统里一个镜像都没有docker rmi会直接报错退出。看起来是小问题但在定时任务里这种错误会导致整条脚本中断后面的告警、日志逻辑全部失效。另外同一镜像可能打了多个 tag。docker rmi myapp:1.0只会移除1.0这个标签镜像本身还在另一个 tag 仍然指向它。只有最后一个 tag 被移除后镜像数据才可能真正释放。所以你在清理后看到空间没有明显变化别怀疑命令错了先用docker images看看是不是同一个 IMAGE ID 还有别的地方引用着。6.4 我日常最常用的几条配置最后分享一下我现在真正每天都在用的几条命令简单但够用# 日常看列表 docker images # 查磁盘占用全景 docker system df # 清理悬空镜像 docker image prune -f # 按仓库批量清理非 latest 版本 docker images --filter referencemyapp/* --format {{.Repository}}:{{.Tag}} | grep -v :latest | xargs -r docker rmi # 输出 JSON 给上层平台 docker images --format {{json .}}docker images看起来只是个看列表的命令但把参数吃透之后它能帮你做筛选、做报表、做自动化清理几乎每一种批量镜像管理需求都能落到它身上。建议你在自己机器上把这些参数各跑一遍尤其是--format {{json .}}和--filter danglingtrue试过才能真正理解输出结构下次写脚本的时候就会顺手很多。