一个机器上跑了几十个容器之后,你一定会遇到这样一个时刻:docker ps刷出来的输出密密麻麻,你只想找到某个镜像启动的服务,或者把所有异常退出的容器一次性清理掉。默认的输出既塞不下你要的信息,也筛不出你要的范围。这时候,--filter和--format这两个参数就派上用场了。前者让 Docker 在服务端先按条件过滤,后者让你按自己的排版方式输出需要的信息。这篇文章我把这两个参数的细节、组合用法和我在实际环境里踩过的坑一次性讲清楚,适合正在做容器管理、或者被docker ps刷屏困扰的朋友。
1. 为什么要把 docker ps “加工”一下再用?
1.1 真实场景:几十个容器堆在屏幕上
刚接触 Docker 的时候,docker ps很简单,一条命令就能看懂当前有哪些容器在跑。但当你在生产环境、CI 环境或者一台开发机上同时运行几十个容器时,这个命令的原始输出会变得非常难用。容器名是随机拼接的、镜像名有长有短、状态栏五花八门,你一屏滚下来根本分不清哪些是前端服务,哪些是数据库,哪些已经处于半死不活的退出状态。
我以前在某公司负责一套微服务环境的日常维护,单机上的容器稳定在三十个以上。每天早上到岗第一件事就是打开终端,输入docker ps -a,然后对着满屏输出找“昨天哪个服务挂了”。这个过程靠肉眼扫,效率极低,还容易漏。后来我把筛选和格式化参数用起来之后,整个排查过程从“看十几行输出再人肉对比”变成了“一条命令直接给出答案”。
1.2 两个参数的分工:过滤在服务端,排版在客户端
很多人误以为--filter和--format都是在本地做数据加工,其实它们的执行位置完全不同。
--filter不是简单的管道筛选,它在发请求给 Docker 服务端时,会把过滤条件编码到 API 请求里。换句话说,Docker 守护进程在返回容器列表之前,就已经在内部把不匹配的容器剔除掉了。客户端接到的列表本身就是过滤后的结果。这种做法的好处是:如果一台机器上有上千个容器,你不需要把全部数据拉到本地再筛,网络开销和本地处理开销都小得多。
--format则发生在客户端拿到数据之后。这是一个用 Go 模板语法渲染输出的过程,你可以从返回结果里挑字段、拼字符串、做条件判断、甚至输出成 JSON 结构,直接丢给jq做二次处理。它的本质是“为机器或人定制输出格式”,不是数据筛选。
想清楚这两者的分工,你才能避免犯“用 format 做过滤”这种新手错误。
2. --filter:让服务端先把脏活干了
2.1 filter 的机制:拼错值不报错,只是返回空
--filter的短参是-f,可以多次使用。一个很有意思的坑是:当你把过滤值写错了,Docker 并不会报错,而是直接返回一个空列表。原因很简单——这个参数会被编码成服务端的查询条件,服务端只是拿条件去匹配,它不校验你的值是否“合法”。
比如你写docker ps --filter status=exite(少了一个 d),服务端会认为你在要求“状态是 exite 的容器”,然后老老实实返回空列表。这个行为非常容易误导人,尤其是写脚本的时候,明明容器就在那里,但脚本拿到的列表是空的,最后排查了半天发现是单词拼错了。
所以,如果你发现自己加了 filter 之后什么都查不到,第一步不是怀疑环境、不是怀疑权限,而是检查你的过滤值本身。多写几个状态值做交叉验证,比如docker ps -f status=running -f status=exited -a,看看是不是 filter 字段本身有问题。
2.2 常用过滤维度速查
Docker 官方支持和实操中常用的过滤 key 大致有下面这些,我把它们整理成了表格,方便你查。
| 过滤 key | 作用说明 | 示例 |
|---|---|---|
name | 按容器名筛,支持子串和正则 | -f name=web |
id | 按容器 ID 前缀筛 | -f id=1a2b3c |
status | 按运行状态筛 | -f status=exited |
label | 按标签键或键值对筛 | -f label=env=prod |
ancestor | 按镜像名、镜像 tag 或镜像 ID 筛 | -f ancestor=nginx |
before | 筛选在该容器之前创建的容器 | -f before=web-old |
since | 筛选在该容器之后创建的容器 | -f since=web-old |
volume | 按挂载的命名卷筛 | -f volume=my-volume |
network | 按接入的网络筛 | -f network=my-net |
expose | 按容器暴露端口筛 | -f expose=8080 |
publish | 按发布到宿主机的端口筛 | -f publish=8080 |
health | 按健康检查状态筛 | -f health=unhealthy |
status可取的值包括created、restarting、running、removing、paused、exited、dead。初学者最容易搞混的是exited和stopped——Docker 里没有stopped状态,停止的容器统一是exited,理解这一点对写自动清理脚本很关键。
label的使用也要留意。-f label=env是筛“有 env 这个键的容器,不看值”;-f label=env=prod则是筛“键值都匹配的容器”。这个区分在做批量标签管理时非常实用。
2.3 多条件组合:同 key 是 OR,不同 key 是 AND
--filter可以传多次,但组合规则很多人说不清楚。其实规则很简单:不同 key 之间是 AND,同一个 key 的多个值是 OR。
举个例子:
docker ps -f status=running -f status=exited -a上面这种写法会同时返回 running 和 exited 的容器,因为这两个条件都是status,是或的关系。
再看:
docker ps -f status=running -f name=nginx这个则是“状态是 running 且名字包含 nginx”的容器,条件之间是与的关系。
这个语义差异决定了你写筛选命令的方式。如果你想表达“名字含 a 或名字含 b”,不能直接写-f name=a -f name=b,那样反而会把条件收窄。正确做法是用 Docker 支持的正则,写成-f name=a|b。
2.4 正则和隐式行为:两个容易忽略的点
name过滤器是支持正则的,这个特性在做批量匹配的时候非常省事。比如你想查所有名字以api开头、后面跟数字的服务容器,可以写:
docker ps --filter "name=^api-[0-9]+"注意,官方容器名在部分场景下会有斜杠前缀,例如早期版本匹配到的是/api-1。所以更稳妥的正则写法是允许斜杠存在:name=^/?api-[0-9]+。
还有一个隐性行为必须说清楚:docker ps默认只看 running 状态的容器,不加-a是看不到exited容器的。但当你使用-f status=exited这类非 running 状态条件时,Docker 会隐含地跳出“只看 running”的限制,直接按你的条件去所有容器里找。这就是为什么docker ps -f status=exited不加-a也能列出退出容器。如果你用-f name=xxx去筛一个已经退出的容器,那不加-a是看不到的。
3. --format:用 Go 模板定制输出
3.1 模板字段与默认输出的坑
--format接受的是一段 Go 模板字符串,核心语法是{{.字段名}}。但这里的字段名和你在docker ps默认输出里看到的列名不是一一对应的。下面这张表是常用的字段,我标出了容易踩坑的地方。
| 模板字段 | 输出内容 | 注意事项 |
|---|---|---|
{{.ID}} | 容器完整 ID | 不是 12 位短 ID,而是一长串 64 位哈希 |
{{.Names}} | 容器名 | 本质是字符串数组,直接打印会出现方括号 |
{{.Image}} | 镜像名和 tag | 如nginx:latest |
{{.Command}} | 启动命令 | 可能带双引号,需要再处理 |
{{.Status}} | 状态和时长 | 如Up 2 hours或Exited (1) 10 minutes ago |
{{.Ports}} | 端口映射 | 多个端口时是拼接在一起的 |
{{.Labels}} | 标签 map | 要取值必须用index |
{{.Networks}} | 网络名 | 字符串数组,多个网络类似[a b] |
{{.Mounts}} | 挂载信息 | 用表格模式可读性更好 |
其中最容易劝退新人的是{{.Names}}。你在模板里写.Names,输出却可能是[web-prod-1 web-prod-2],带着方括号和空格。原因是这个字段在 Docker API 返回的数据结构中是字符串数组,Go 模板在渲染数组时默认加了[ ]。想要干净名字,要么在模板里用join函数,要么在管道后面用awk做二次清洗。
再说 ID。{{.ID}}输出的是完整哈希,如果你想让它和默认输出的 12 位短 ID 保持一致,必须用trunc函数截断。
docker ps --format '{{.ID | trunc 12}} {{.Names}}'这个细节很容易被忽视,我第一次写格式化脚本时直接把 ID 打到日志里,一行能有六十多个字符,严重干扰阅读。
3.2 模板函数的实测用法
Docker 的模板引擎内置了一批函数,常用到的有三类:
第一类是字符串处理函数。trunc截断、upper转大写、lower转小写、title首字母大写。比如:
docker ps --format '{{.Image | upper}}'第二类是格式化函数。printf可以在模板里做列宽对齐:
docker ps --format '{{printf "%-20s %s" .Names .Status}}'这里的%-20s是 Go 的格式化语法,表示左对齐、占 20 个字符宽度。输出会比默认格式整齐不少。
第三类是 JSON 输出函数。{{json .}}会输出整个容器信息的 JSON 结构,这个在调试时非常好用:
docker ps --format '{{json .}}' | jq .Names有了它,你就不用打开docker inspect去查字段结构,先把当前语法能拿到的字段全部 dump 出来,再人肉决定要哪几个。
3.3 table 模式:带表头又不费事
如果想快速生成一个带列标题的报表,用table前缀就行:
docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}'输出会自带NAMES、IMAGE、STATUS三个表头,列与列之间用 Tab 分隔。这个格式在终端下阅读很舒服,也能直接保存成 CSV 给后续表格工具处理。注意table模式下的表头是根据字段名自动生成的,想看中文表头只能自己打印一行,比如:
echo -e "容器名\t镜像\t状态" docker ps --format '{{.Names}}\t{{.Image}}\t{{.Status}}'3.4 Labels 取值的正确姿势
{{.Labels}}是一个 map 类型,直接打印会把所有键值对一起打出来,既难读也不好在脚本里取某个值。正确方式是用index函数:
docker ps --format '{{.Names}} env={{index .Labels "env"}}'这个写法可以拿到容器指定 label 的值。如果 label 键不存在,会输出<no value>。想更干净一点,可以配合条件判断:
docker ps --format '{{if .Labels}}有标签{{else}}无标签{{end}}'条件判断在写监控脚本时很有用,比如根据 env 标签决定打什么颜色标记,或者根据 labels 是否存在做后续处理。
4. 组合实战:三条可以直接抄走的命令
4.1 一次性清理 exited 僵尸容器
我见过很多人手动清理退出容器,一条条docker rm,效率低且容易误删。配合status过滤和format拿 ID,可以做一条安全又干净的清理命令。
docker ps -a --filter status=exited --format '{{.ID}}' | xargs -r docker rm这里的-r是 GNU xargs 的选项,表示“输入为空时不执行后面的命令”。如果你的环境是 macOS 自带的 BSD xargs,不需要这个参数。整段命令的原理很简单:先筛出所有 exited 状态的容器,用--format '{{.ID}}'只输出容器 ID,再交给xargs docker rm逐个删除。
实际使用中,我还会在正式执行前先打印一遍要删的容器名,确认没有误伤:
docker ps -a --filter status=exited --format '{{.Names}}'确认无误后再执行删除。这个习惯在批量操作里永远值得保留。
4.2 按项目标签生成运行状态报表
如果容器在启动时都打了project标签,按标签生成一张运行状态表非常简单:
docker ps --filter label=project=web --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'我通常还会在这个基础上加一个状态统计脚本,这样每天巡检不用一条条数容器,直接看汇总:
for s in created restarting running removing paused exited dead; do n=$(docker ps -a --filter status=$s -q | wc -l | tr -d ' ') echo "$s: $n" done这个脚本的本质是对前面说的status过滤做遍历汇总。跑完之后,哪一类容器数量异常多,一眼就能看出来。我把它存成了/usr/local/bin/container-stats,巡检的时候就跑一下,比docker stats在全局视角上清晰得多。
4.3 批量重启/停止跑飞的服务容器
服务的健康检查挂了、容器状态变成 unhealthy,但还在 running,这时候需要批量重启。用 label 加 status 组合筛出目标容器再重启:
docker ps --filter label=service=gateway --filter status=running --format '{{.ID}}' | xargs -r docker restart如果希望更精准,还可以再加上health条件:
docker ps --filter label=service=gateway --filter health=unhealthy --format '{{.ID}}' | xargs -r docker restart这套命令的价值在于:把“人肉找容器名”变成“机器按标签筛容器”。容器名是随机变化无所谓,只要标签固定,操作逻辑就是稳定的。这也是我强烈建议每个容器启动时都打统一标签的原因。
5. 排错实录:filter 和 format 的常见坑
5.1 filter 没生效?先检查这几件事
第一条已经提过:值拼错了不报错,返回空。这个坑占比最大。
第二条是状态过滤和-a的纠缠。想筛非 running 容器时,建议统一加-a,不要依赖 Docker 的隐式行为,不同版本的 Docker 在边界场景处理上不完全一致,显式加上最稳妥。
第三条是条件语义搞反。区分“同 key OR,不同 key AND”这个规则后,很多看似奇怪的结果都能解释。比如你想匹配“服务 A 或服务 B”,写成-f name=a -f name=b结果为空,这不奇怪,因为这是“名字同时含 a 和 b”的容器,根本不可能有。
还有一类隐蔽问题:filter 的 key 是大小写敏感的。-f Name=foo不是-f name=foo,写错了会被当作未知过滤条件处理,返回空。
5.2 format 字段写错了会怎样?
--format对字段名的校验很宽松。写了一个不存在的字段,它不会直接报错,而是悄悄输出<no value>。比如有些人记不住Status的大小写,写成{{.status}},结果整行输出全是<no value>,还不知道问题出在哪。
应对方法有两个:一是先跑一遍docker ps --format '{{json .}}',把字段的真实拼写和类型 dump 出来,对照着看;二是写模板后用小范围容器做线下验证,不要直接在生产环境刷屏。
5.3 其他值得记住的小坑
docker ps --format '{{.Names}}'输出的是数组形式,如果要写入监控系统或日志,建议先用awk清洗:
docker ps --format '{{.Names}}' | awk -F'[][]' '{print $2}'这样能把[web-prod-1 web-prod-2]里的内容抽出来。如果名字之间有空格,后续可以再接tr ',' ' '统一分隔符。
再一个是-q和--format的组合问题。-q明确要求“只输出 ID”,很多版本下它会忽略--format的字段定制,所以别指望docker ps -q --format '{{.Names}}'能输出名字。要么用-q,要么用--format,二者不要混着来。
最后提醒一下:模板里用printf做对齐时,如果容器名超过设定的宽度,Go 的格式化不会截断,只会把你设定的宽度撑破,输出看起来依然参差不齐。解决方式是先trunc再printf:
docker ps --format '{{printf "%-20s" (.Names | trunc 20)}}'这种做法在生成对齐报表时特别有用。
我在实际环境里踩过几次坑之后,已经把这套 filter 和 format 的组合当成自己容器管理工具箱里的默认配置,特别是把常用的几条存成了 shell 函数,比如ps-running、ps-exited、ps-clean,每次只要敲一个简单的别名就能拿到想要的视图。这个做法你也可以试试:先花十分钟根据自己手头容器的标签和命名规则定制几条命令,然后固化下来,长期受益远比你想的大。