1. 先搞清楚 docker ps 在整套容器操作里的位置
docker ps大概是所有人学 Docker 敲的第一条命令,也是日常敲得最多的一条。但绝大多数人对它的认知停留在"看容器有没有跑起来",等到真出问题——容器莫名其妙没了、端口占不上、磁盘被写满、线上某个服务悄悄重启过三次——才发现这条命令的默认输出根本不够用。我见过太多人在故障现场反复刷docker ps刷不出线索,最后绕一大圈去翻宿主机日志,其实答案就藏在docker ps -a加两个过滤器的结果里。
这篇内容专门把docker ps这一条命令拆开讲透:它查的是什么数据、七列输出每一列的含义和陷阱、--format模板怎么写、--filter的完整过滤器清单和组合逻辑、以及我这些年用它做故障排查的真实链路。目标读者是已经能把容器跑起来、但还没把这条命令用到顺手的中级使用者;完全没接触过 Docker 的朋友也能看懂,只是需要边看边跟着敲。
需要强调一点:docker ps本质上是查询 Docker 守护进程维护的容器元数据快照,不是实时监控,也不是日志工具。它告诉你"此刻守护进程认为这个容器处于什么状态",而这个"认为"有时候和你肉眼看到的服务状态并不一致。理解这一点,后面很多反直觉的现象就都有解释了。
1.1 它到底查的是什么
Docker 守护进程内部对每个容器都维护一份状态记录,包括容器 ID、所用镜像、启动命令、创建时间、运行状态、端口映射、挂载卷、网络归属、标签等。docker ps做的就是把这份记录按你指定的格式打印出来。它不发请求到容器内部,不读容器的文件系统,也不关心容器里跑的业务是不是真的在正常服务——只要守护进程标记它为running,它就显示Up。
这解释了一个经典场景:容器里的进程卡死在死循环里,CPU 打满、HTTP 完全无响应,但docker ps依然显示Up 3 hours。这时候该用docker stats看资源,用docker exec进去看进程,或者看健康检查状态,光看docker ps会被骗。反过来说,如果容器配了HEALTHCHECK,docker ps的 STATUS 列会带上(healthy)或(unhealthy)后缀,这是它唯一能反映"业务层"状态的入口,非常值得用起来。
另一个常见误解是docker ps会去连远程的某个地方。它只连接当前 Docker 上下文指向的那个守护进程,也就是DOCKER_HOST环境变量或者docker context当前选中的目标。你在一台机器上装了两套 Docker(比如系统级一套、用户级一套),docker ps结果不一样是正常的,不是 bug。
1.2 谁最该把这条命令吃透
三类人收益最大。第一类是负责部署和运维的同学,日常要在一台宿主机上管理几十上百个容器,靠docker ps的默认输出找人,眼睛都看花;学会--format之后一条命令直接输出"名字 + 状态 + 端口 + 重启次数",效率差一个量级。第二类是排查线上问题的开发,容器退出后默认看不见,必须知道-a和-f status=exited的组合,还要能从退出码 137 立刻判断出是被 OOM 干掉了还是被docker kill干掉了。第三类是写自动化脚本的人,CI 流水线里判断容器是否就绪、清理构建残留、批量导出容器清单,都绕不开-q加管道这套组合拳。
还有一类容易被忽略的:做本地开发的同学。本地环境容器多、启停频繁,最容易堆出一堆Exited (0)的僵尸容器,日积月累占掉几十 GB 磁盘,docker ps -a一眼就能看出来,配合docker container prune十分钟解决。
2. 默认输出七列逐列拆解
不带任何参数执行docker ps,你会看到七列。这七列里至少有三列藏着我踩过坑的细节,值得逐列说清楚。
2.1 CONTAINER ID 与 NAMES 的选取逻辑
CONTAINER ID 默认截断成 12 位十六进制字符。截断的目的是人眼可读,理论上存在碰撞可能,实际概率极低,但如果你在写脚本做容器 ID 匹配,建议加--no-trunc拿完整的 64 位,避免碰撞导致的误操作——尤其是批量删除这种不可逆的操作,多打一个参数换一份安心。
NAMES 列的水比想象中深。容器名在同一个守护进程内必须唯一,重复了会直接报Conflict. The container name "/xxx" is already in use。但"唯一"的范围仅限当前守护进程,换个 context 就互不干扰。另外,用docker run不指定--name时,守护进程会自动生成一个名字,格式是两个随机单词拼接,这种名字在排查时基本没有信息量,所以我强烈建议所有docker run都显式带--name,而且起名规则统一,比如业务名-环境-序号。
用 Compose 起的容器,NAMES 会是项目名-服务名-序号的形式,同时容器上会打一串com.docker.compose.*标签。这意味着你可以用docker ps -a --filter "label=com.docker.compose.project=myproj"精确捞出某个 Compose 项目的所有容器,比按名字前缀猜要靠谱得多,后面讲 label 过滤时会展开。
还有个小细节:NAMES 列有可能显示多个名字。当一个已停止的容器被docker run --name newname复用某个旧容器时,旧名字会作为别名挂上去,这时列里会用逗号分隔列出多个名字。看到这种现象不用惊讶,说明这个容器被改名过或者被链接过。
2.2 STATUS 列是信息量最大的一列
这一列是整条命令里最值钱的,也是最容易被一眼扫过去的。它的取值形态大致有这几种:
| STATUS 显示 | 含义 | 通常对应的问题 |
|---|---|---|
Up 3 minutes | 运行中,无健康检查 | 正常 |
Up 2 hours (healthy) | 运行中,健康检查通过 | 正常 |
Up 5 minutes (unhealthy) | 运行中,健康检查连续失败 | 业务进程假死、依赖不可达 |
Up 10 seconds (health: starting) | 健康检查还在初始宽限期 | 刚启动,尚无定论 |
Exited (0) 2 hours ago | 正常退出 | 一次性任务跑完,或主进程收到优雅退出信号 |
Exited (137) 5 minutes ago | 被 SIGKILL 强杀 | 最典型是内存超限被 OOM Killer 干掉 |
Exited (143) 1 hour ago | 被 SIGTERM 终止 | docker stop超时前进程正常响应 |
Restarting (1) 8 seconds ago | 反复重启中 | 启动即崩,配置错或依赖没起来 |
Paused | 被docker pause挂起 | 手动操作或调试遗留 |
Created | 已创建但从未启动 | 建了没跑,常见于脚本中断 |
Dead | 守护进程尝试删除但失败 | 存储层异常,需要手工介入 |
Removing | 正在删除 | 卡在这个状态说明有进程占用 |
退出码这一块值得单独记。容器主进程的退出码是判断故障性质最快的线索:0一般说明任务正常结束,如果你的常驻服务出现Exited (0),那多半是主进程写成了"跑完就退"的模式,比如把守护进程当前台任务启动了;1通常是应用自身抛异常退出,得去看日志;137是128 + 9,代表被 SIGKILL 杀掉,最常见的原因是内存超限,其次是docker kill或者宿主机 OOM;143是128 + 15,代表收到 SIGTERM 后正常退出,docker stop默认就是先发 SIGTERM 等 10 秒再 SIGKILL,所以看到 143 说明进程响应了优雅退出;255一般是启动命令本身执行失败,比如 entrypoint 脚本里的命令不存在。
我个人的习惯是:看到Restarting先别急着看日志,先docker ps -a --filter "name=目标容器"确认退出码,137 就直接去查内存限制和宿主机dmesg,1 和 255 就去看应用日志和启动脚本,路径完全不同,能省一大半时间。
2.3 PORTS 列的三种形态与含义
PORTS 列只展示发布到宿主机的端口映射,格式是宿主IP:宿主端口->容器端口/协议。常见形态有三种:
第一种是0.0.0.0:8080->80/tcp,表示宿主机所有 IPv4 地址的 8080 都转发到容器 80。这是-p 8080:80的默认效果。第二种是127.0.0.1:8080->80/tcp,只监听本地回环,说明启动时写了-p 127.0.0.1:8080:80,这种配置在只想本机访问、不想暴露到外网的场景下非常合适,但经常有人忘了自己这么写过,然后远程连不上就开始怀疑防火墙。第三种是 IPv6 的双份映射0.0.0.0:8080->80/tcp, [::]:8080->80/tcp,这是宿主机开启了 IPv6 时 Docker 自动加的行为,看到两条不算重复映射。
多个端口映射之间用逗号分隔。如果容器有端口但没做映射,这一列就是空的——注意,镜像里的EXPOSE 80不等于把端口发布到宿主机,它只是一条元数据声明,方便-P随机映射时知道该映射哪些端口。很多人以为EXPOSE写了就能访问,结果 PORTS 列空空如也,白查半天。要确认容器到底声明了哪些端口,用docker ps --format '{{.Ports}}'看不出来,得docker inspect看Config.ExposedPorts。
反过来,如果 PORTS 列有映射但服务连不上,排查顺序应该是:先docker exec进容器curl localhost:容器端口确认容器内服务活着,再看宿主机ss -lntp | grep 宿主端口确认监听存在,最后才怀疑网络层。这个顺序能砍掉大部分无效排查。
3. 输出控制:把结果改造成你要的样子
默认七列是为"人随便扫一眼"设计的,一旦容器数量上到两位数,这七列就会互相挤压、换行、看不清。--format是这条命令真正的生产力所在,值得花时间掌握。
3.1 --format 的模板语法到底怎么写
--format接受 Go 模板语法,基础用法是{{.字段名}}。可以用的字段包括.ID、.Image、.Command、.CreatedAt、.RunningFor、.Ports、.State、.Status、.Size、.Names、.Labels、.Mounts、.Networks、.LocalVolumes。其中.CreatedAt是绝对时间,.RunningFor是相对时间(就是默认 CREATED 列那个"3 minutes ago")。
字段之间的分隔靠\t(制表符)和\n(换行)。想让输出带表头,在模板前面加table关键字,Docker 会自动把字段名转成人能读的列名并加上分隔线。这个细节很多人不知道,导致每次都手写表头。
获取单个标签的值要用函数调用形式{{.Label "键名"}},注意这里不是.Labels那种 map 输出,而是精确取值,取不到时输出空字符串。这个语法在筛 Compose 容器时特别好用,可以直接把com.docker.compose.service拉出来当一列。
还有一个隐藏神器:{{json .}}。它把整条记录序列化成一行 JSON,配合jq就是无限可能。这个我下面单独说。
注意:模板里的字段名大小写敏感,
.Names不是.Name,.ID是全大写,写错了不会报错,只会静默输出空值——这是最容易让人怀疑人生的地方。建议第一次写模板时先用{{json .}}把完整字段打印出来,照着抄。
3.2 我常驻收藏的几条输出模板
下面这几条是我在若干台机器上配了 alias 的,直接抄走改改就能用。
日常总览,一屏看完全部容器的关键信息:
docker ps -a --format "table {{.Names}}\t{{.State}}\t{{.Status}}\t{{.Ports}}"只看名字和状态,用于确认某批服务是否都在跑:
docker ps --format "table {{.Names}}\t{{.Status}}"排查 Compose 项目,把服务名和项目名单独拉成列:
docker ps -a --format "table {{.Names}}\t{{.Label \"com.docker.compose.project\"}}\t{{.Label \"com.docker.compose.service\"}}\t{{.Status}}"按镜像版本盘点,确认哪些容器还跑着旧版本:
docker ps --format "table {{.Image}}\t{{.Names}}\t{{.RunningFor}}" | sort只输出容器名,喂给后续脚本:
docker ps --format "{{.Names}}"这里有个小技巧:模板字符串用单引号包裹时,内部的双引号不用转义,写起来清爽很多,docker ps --format 'table {{.Names}}\t{{.Label "com.docker.compose.service"}}'效果完全一样。我在脚本里统一用单引号,避免 shell 二次解析带来的意外。
3.3 机器可读输出与管道协作
人工排查用模板,脚本处理就该用 JSON。docker ps --format '{{json .}}'每条容器输出一行 JSON,天然适合jq处理。举几个实战例子。
找出所有运行中容器的名字和镜像,输出成 JSON 数组:
docker ps --format '{{json .}}' | jq -s 'map({name: .Names, image: .Image, state: .State})'统计各镜像跑了多少个容器:
docker ps --format '{{.Image}}' | sort | uniq -c | sort -rn把容器 ID 批量喂给docker inspect拿详细信息:
docker ps -q | xargs docker inspect --format '{{.Name}} {{.HostConfig.Memory}}'批量对运行中容器执行某操作,比如统一看时间:
docker ps -q | xargs -r -n1 docker exec {} date-q只输出容器 ID,是"纯数据模式"的开关,和-a组合就得到全部容器的 ID 列表。注意xargs一定要加-r(GNU 版本),否则列表为空时它会不带参数执行一次命令,某些命令在无参数时行为危险。
提示:
docker ps -q在没有匹配结果时输出为空,$(docker ps -q)会展开成空字符串,导致docker rm $(docker ps -q)变成无参数的docker rm,直接报错。用docker ps -q | xargs -r docker rm更稳妥。
4. 过滤器 --filter 的完整用法
--filter(简写-f)的过滤发生在守护进程侧,也就是说匹配逻辑在服务端完成,返回的就已经是筛选后的结果。容器数量多的时候,这比拉全量回来再grep高效得多,也更准确——grep会把镜像名、命令里碰巧出现的字符串也算进去。
4.1 按状态与退出码筛
status过滤器支持的值包括created、running、paused、restarting、removing、exited、dead。默认docker ps等价于docker ps --filter status=running,这也是为什么加-a才能看到退出容器。
找所有异常退出的容器(排除正常退出的 0):
docker ps -a --filter status=exited这个只能筛出"退出了",不能筛退出码。要按退出码筛,得用exited过滤器,它直接接受一个整数:
docker ps -a --filter exited=137这条命令的实战价值极高:容器被 OOM 杀掉之后,业务方往往只报告"服务挂了",你把 137 的容器全捞出来,一眼就知道是不是内存限制配小了。同理--filter exited=1能快速定位"启动即崩"的那批。
health过滤器是另一个被严重低估的:--filter health=unhealthy直接列出所有健康检查失败但还在跑的容器。如果你的服务配了健康检查,这一条基本就是"线上正在带病运行"的清单,建议做成定时巡检任务。
docker ps --filter health=unhealthy --format 'table {{.Names}}\t{{.Status}}'4.2 按名字、镜像、端口筛
name过滤器最容易踩坑的地方是它做的是模糊匹配。--filter name=web会命中web、web-1、myweb、webhook-worker,全都算。想要精确匹配得用正则锚定:
docker ps -a --filter "name=^web$" docker ps -a --filter "name=^web-"前一条精确匹配名字为web的容器,后一条匹配所有以web-开头的容器。这个区别在批量操作时非常致命——你以为在删测试环境的api,结果把api-gateway-prod一起带走了。
ancestor过滤器按镜像筛,支持镜像名、镜像:标签、镜像 ID。它有个特性:会同时匹配基于该镜像构建出的后代镜像所创建的容器。比如--filter ancestor=node:18不只匹配直接跑node:18的容器,还会匹配用FROM node:18构建出来的应用镜像起的容器。这个行为大部分时候是便利,偶尔会造成"怎么多出来几个"的困惑。
docker ps -a --filter ancestor=nginx:1.24按端口筛有两个容易混淆的过滤器。publish匹配的是宿主机侧发布的端口,expose匹配的是容器侧声明的端口:
docker ps --filter publish=8080 docker ps --filter expose=80端口占用排查时用publish,想知道"谁占了宿主机 8080"一条命令搞定,比ss -lntp再反查进程 ID 再对到容器快得多。
还有volume、network、is-task、before、since这几组。volume用来找"哪个容器挂了我这块盘",磁盘告警时特别好用:
docker ps -a --filter volume=data-volnetwork用来定位同一网络下的容器,做网络连通性排查时先圈定范围:
docker ps --filter network=app-netis-task=true筛的是集群服务的任务容器,单机场景用不上。before和since接受容器名或 ID,用来按创建时间做相对筛选,比如"比某个容器更晚创建的都列出来",在排查"部署顺序错了"这类问题时很好使。
4.3 组合过滤的 OR / AND 逻辑
这是最容易出错的地方,必须记牢:同一个过滤器键出现多次是 OR 关系,不同过滤器键之间是 AND 关系。
举例,想同时看运行中和暂停的容器:
docker ps -a --filter status=running --filter status=paused两个status之间是 OR,等价于"状态是运行中 或者 暂停"。
想找某个镜像里、处于运行状态的容器:
docker ps --filter ancestor=myapp:1.2 --filter status=running不同键之间是 AND,等价于"镜像是我的应用 并且 状态是运行中"。
理解了这套逻辑,复杂查询就能直接组合出来。比如"某个 Compose 项目下、健康检查失败、且还在运行"的容器:
docker ps \ --filter "label=com.docker.compose.project=myproj" \ --filter health=unhealthy \ --filter status=running注意:老版本 Docker 对同键多值 OR 的支持有过差异,如果发现结果不符合预期,先用
docker version确认版本,实在不行拆成多条命令用cat合并。另外--filter的键值对里带等号时要小心,label=k=v里第一个等号是键值分隔,后面全算值的一部分,所以label=env=prod是正确的,不会报错。
4.4 用 label 把业务维度带进查询
前面反复提到 label,这里系统说一下,因为这是我用docker ps效率提升最大的一环。容器标签是一组键值对,启动时用--label打上,之后就能当查询维度用。
docker run -d --name api-1 \ --label env=prod \ --label app=api \ --label owner=team-a \ myapp:1.2之后要查"生产环境所有 api 服务":
docker ps --filter "label=env=prod" --filter "label=app=api"要按标签取值做列输出:
docker ps --format 'table {{.Names}}\t{{.Label "env"}}\t{{.Label "owner"}}'只判断标签键存在,不关心值时,写--filter label=env就行,不带等号。
这套做法的价值在于:名字是给人看的,容易变、容易乱;label 是给机器看的,可以严格规范。我在实际项目里会强制三组标签——环境、应用名、负责人,起容器时通过部署脚本自动注入。出问题时不管是"哪个应用占了太多内存"还是"这批容器该找谁",一条带 label 过滤的docker ps就能拉出清单,比翻部署记录快得多。
另外 Compose 会自动打上一组标签,除了前面说的project和service,还有com.docker.compose.project.working_dir(工作目录)、com.docker.compose.project.config_files(配置文件路径)、com.docker.compose.container-number(容器序号)、com.docker.compose.oneoff(是否是一次性容器)。working_dir在排查"这份容器是从哪个目录起的"时特别管用,同名项目散落在多个目录的情况下,一眼就能分清。
5. 真实排查链路:从一条异常记录往下挖
光讲参数没意思,讲个完整链路,都是我实际碰过的情况。
5.1 容器“神秘消失”的三种情况
经常有人问:昨天还在跑的容器,今天docker ps里没了,是不是被谁删了?
先别慌,docker ps默认只显示运行中的容器。第一步永远是加-a:
docker ps -a --filter "name=^myapp$"如果-a里能看到,状态是Exited,那说明容器还在,只是进程退了,看退出码往下查。如果-a里也看不到,才是真消失了。
真消失的原因无非三种。第一种是启动时带了--rm,容器一退出自动删除,这是最常被忽略的。这类容器在故障排查时是灾难,因为日志跟着一起没了,所以我在生产环境严禁使用--rm,只在一次性脚本容器里用。
第二种是被清理掉了。docker container prune会批量删除所有已停止的容器,docker system prune范围更大,连未使用的网络和镜像一起清。这两种操作经常是有人在清理磁盘时顺手执行的,事后没人记得。判断方法很简单:看磁盘使用率是不是突然降下来了,或者查一下操作记录。
第三种是守护进程重启导致状态丢失。虽然 Docker 本身设计上重启后能恢复容器状态,但如果容器是--restart策略为no并且进程异常退出,重启后它就停在Exited状态,需要人工拉起。这种时候用docker ps -a --filter status=exited一把捞出来,比一个个找快得多。
还有一种特殊情况:容器进入了Dead状态,同时docker rm删不掉,报设备忙或者层被占用。这通常是存储层出了问题,或者有进程还开着容器内的文件句柄。我的处理顺序是:先docker ps -a --filter status=dead确认范围,再重启守护进程试一次,还不行就去找占用文件的进程,最后手段才是动存储目录。整个过程里docker ps的作用是确认"问题范围到底有多大",避免误判成一个容器的问题最后发现是一批。
5.2 端口与名字冲突的定位过程
另一个高频现场:部署脚本报错Bind for 0.0.0.0:8080 failed: port is already allocated,或者Conflict. The container name "/api" is already in use。
端口冲突的定位:
docker ps --filter publish=8080 --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}'一条命令直接告诉你是谁占了。如果这条命令返回空,说明占用者不是 Docker 容器,这时候才需要回到宿主机层面用ss -lntp | grep 8080去找。这个判断顺序很重要——先排除 Docker 内部占用,再往外查,能避免在宿主机上白翻半天。
名字冲突的定位:
docker ps -a --filter "name=^api$" --format 'table {{.Names}}\t{{.Status}}\t{{.RunningFor}}'看到结果后再决定怎么处理。如果是Exited状态的老容器,说明上次部署没清理干净,docker rm掉重来;如果是Up状态,那可能是上一次部署的容器还活着,得先确认它是不是在被使用,别直接删。我见过最糟糕的情况是有人的部署脚本写成了"先 run 新容器再 rm 旧容器",结果新容器因为名字冲突起不来,旧容器也没删掉,脚本还返回成功,最后线上跑的是两周前的版本。这类问题靠docker ps -a里的RunningFor一列就能立刻发现——显示Up 2 weeks而你以为刚部署,问题一目了然。
5.3 批量清理与风险控制
清理是docker ps最实用的批量场景,但也是最容易出事故的地方。我的原则是:先看后删,永远先把结果打印出来确认。
清理所有已退出的容器,第一步先看:
docker ps -a --filter status=exited --format 'table {{.Names}}\t{{.Status}}\t{{.Image}}'确认清单没问题,再执行删除:
docker ps -aq --filter status=exited | xargs -r docker rm注意这里没有用-f(强制删除运行中的容器的那个-f),因为筛选条件已经限定为exited,运行中的容器不会进列表。但如果你写成docker ps -aq | xargs -r docker rm,漏了过滤器,那docker rm会拒删运行中的容器并报错,虽然不会造成破坏,但会中断整个批量流程,后面的容器都删不掉。
按时间清理,只删 24 小时以前的退出容器:
docker ps -a --filter status=exited --format '{{.ID}} {{.RunningFor}}'RunningFor是相对时间文本,不太好直接做数值比较。这种场景我更推荐用--filter since配合一个基准容器,或者干脆用docker ps -a --format '{{.CreatedAt}} {{.ID}}' | sort之后按字符串前缀手动截取日期部分。虽然笨,但可靠,而且不会误删。
提示:清理类命令在执行前建议先
| tee /tmp/cleanup-$(date +%s).log留一份记录。真删错了至少还能知道删了什么,去重建的时候有个对照。这个习惯帮我挽回过的次数不止一次。
6. 高频问题速查表与几个反直觉细节
6.1 问题速查表
| 现象 | 可能原因 | 快速定位命令 |
|---|---|---|
| 容器列表为空 | 只看运行中;查错了 context | docker ps -a;docker context ls |
| 报 permission denied | 当前用户不在 docker 组 | groups,需要加入 docker 组 |
| 明明有容器却搜不到 | name模糊匹配把结果筛掉了 | 改"name=^精确名$"再试 |
| 端口映射列空白 | 只写了EXPOSE,没做-p | docker ps --format '{{.Ports}}'确认 |
| STATUS 无健康后缀 | 镜像没有HEALTHCHECK | docker inspect看Health字段 |
| 时间对不上 | CREATED 是相对时间 | 改用{{.CreatedAt}}看绝对时间 |
| 命令列被截断成省略号 | 默认截断显示 | 加--no-trunc |
| 容器反复重启 | 启动即崩,依赖未就绪 | docker ps -a --filter status=restarting |
| 磁盘越用越少 | 僵尸容器累积 | docker ps -a --filter status=exited |
-s卡顿明显 | 要统计写层大小,实时计算 | 降频使用,或只用docker system df看总量 |
6.2 几个我踩过的坑
第一个坑:--size会明显变慢。docker ps -s会在 Size 列多输出一个写层占用和虚拟大小,前者需要遍历容器的写层目录统计,容器一多就卡。我之前在一台跑了两百多个容器的机器上随手加了-s,命令卡了七八秒才出来。要整体看磁盘占用,用docker system df更合适,它是按镜像层和卷做聚合统计的,速度快得多。
第二个坑:docker ps的输出是点时刻快照。想持续观察容器状态变化,得写成循环或者直接上docker events。docker events --filter type=container --filter event=die能实时捕获容器退出事件,抓到之后立刻docker ps -a --filter "id=容器ID" --no-trunc看详情,这是我认为排查"偶发退出"最有效的一套组合,比事后翻记录强太多。
第三个坑:容器内执行docker ps看到的是宿主机容器。如果容器挂载了 Docker 的 socket 文件(很多 CI 和构建场景会这么做),进去执行docker ps能看到宿主机上的所有容器。这经常让新人困惑"为什么容器里能看到别的容器",其实它是在查询宿主机的守护进程,跟容器自身无关。判断方法:在容器内ls -l /var/run/docker.sock,能看到这个文件并且有权限,那就是挂进来的。
第四个坑:--filter ancestor匹配后代镜像让统计失真。前面提过这个特性,做容量盘点时它会带上所有基于基础镜像构建出来的应用容器,导致"这个基础镜像怎么跑了几十个容器"的错觉。要精确统计直接使用某镜像的容器,还是老老实实比对{{.Image}}输出。
第五个坑:docker ps里的 ID 前缀可能在极个别情况下不唯一。12 位十六进制在正常规模下几乎不可能碰撞,但如果你在写自动化脚本,坚持用--no-trunc或者直接用-q,避免任何"用前缀去匹配"的逻辑。这个坑我自己没踩过,但见过同事的脚本在多节点环境下误删过容器,代价很大。
第六个坑:默认输出里没有重启次数。判断容器稳定性,重启次数比RunningFor更有说服力——一个Up 30 seconds但重启了 20 次的容器,和一个Up 3 days重启 0 次的容器,状态天差地别。重启次数在docker ps的模板字段里拿不到,得走docker inspect --format '{{.RestartCount}}'。我的做法是写个小脚本把两边拼起来,巡检时一把扫完,比单看docker ps完整得多。
把这些串起来看,docker ps的价值不在于"看看有没有跑起来",而在于它是整个容器排查链路的入口。从这个入口拿到名字、状态、退出码、端口、标签,后面不管是查日志、查资源、查网络还是查挂载,方向和范围都是这里确定的。参数就那么几个,但组合方式和判断顺序需要靠实战磨。我自己的习惯是先-a拉全量确认范围,再按 label 或 name 精确收窄,最后--format输出成能一眼看完的表格——三步下来,大部分"容器出问题了"的现场都能在三十秒内锁定方向。