1. 从一次容器消失说起docker ps 到底在看什么凌晨两点一个跑了三个月的老服务突然没响应了。第一反应是敲docker ps屏幕上干干净净一条记录都没有。那一刻的直觉是容器没了但事实往往相反——容器还在只是已经退出了而docker ps默认压根不给你看已退出的容器。这个细节坑过太多人包括我自己早期排查线上问题时硬是重启了一遍服务才反应过来该加-a。docker ps是 Docker 日常使用频率最高的命令之一几乎每次进机器第一件事就是敲它。它做的事情很朴素向 Docker 守护进程查询容器列表然后把结果按列排版打印到终端。但朴素不等于简单这个命令的参数组合、输出字段含义、过滤与格式化能力直接决定了你排查问题时是一眼看穿还是翻半天没头绪。它适合刚接触容器的新手建立对容器生命周期的直观认知也适合已经用了一两年的同学补齐那些平时靠肌肉记忆敲、但从没细究过的部分。我见过不少人把docker ps当成ls来用只看 NAMES 和 STATUS 两列其他全忽略。这样能干活但一旦遇到端口冲突、镜像版本对不上、容器反复重启这类问题就会卡住。真正的价值在于你得知道每一列是怎么来的、哪些参数能改输出、哪些过滤器能把范围缩小到你想找的那一个。这篇文章就把这个命令拆开讲透包括那些文档里写了但你未必会注意的细节以及我在实际排查中踩过的坑。先把最基础的跑起来。在任意装了 Docker 的机器上执行docker ps输出大致是这样CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES a1b2c3d4e5f6 nginx:1.25 /docker-entrypoint.… 3 hours ago Up 3 hours 0.0.0.0:8080-80/tcp web-proxy f6e5d4c3b2a1 redis:7-alpine docker-entrypoint.s… 2 days ago Up 2 days 6379/tcp cache-node七列信息每一列背后都对应 Docker 内部的一个数据结构字段。CONTAINER ID 是容器的唯一标识符默认截断到 12 位显示IMAGE 是启动这个容器所用的镜像名和标签COMMAND 是容器启动时执行的入口命令同样会被截断加省略号CREATED 是容器创建时间注意是创建时间不是启动时间这两个在容器被 stop 再 start 之后就会分叉STATUS 是当前状态加已运行时长PORTS 是端口映射关系NAMES 是容器名字。这里第一个容易误解的点就出来了STATUS 显示的 Up 3 hours 指的是容器从最后一次启动到现在的时间容器如果中途重启过这个时间会归零重算。所以看到 Up 2 minutes 不代表容器是两分钟前创建的只代表它两分钟前启动。这个区别在排查容器是不是刚崩过的时候特别关键。2. 默认只显示运行中容器-a 参数与容器生命周期的关系2.1 为什么默认隐藏已停止的容器Docker 这么设计不是偷懒。一台长期运行的机器上停止状态的容器可能积攒到几百个全列出来会淹没真正在跑的服务。默认只显示 Running 状态的容器是让输出保持当前工作集的语义。从使用者角度看这是合理的但从排查角度看这就是个陷阱。容器有几个状态created创建了但没启动、running运行中、paused被 pause 挂起、restarting重启策略触发中、exited已退出、dead守护进程无法清理的僵尸状态。docker ps默认只显示 running 和 paused 这两种。剩下四种状态必须显式加-a或--all才能看到。docker ps -a加了-a之后输出会多出 Exited 的容器STATUS 列会变成类似Exited (1) 5 minutes ago的样子。括号里那个数字是退出码这是排查崩溃问题最重要的线索之一。退出码 0 表示容器主进程正常结束1 通常是应用自身报错退出137 是收到了 SIGKILL常见于 OOM 被杀或docker kill143 是收到了 SIGTERMdocker stop触发的优雅退出。看到 137 就该去查内存限制了而不是去翻应用日志找语法错误。2.2 经常被忽略的 created 状态容器有一种情况特别迷惑人容器存在但既不在docker ps里也不在docker ps -a的 Exited 里而是显示为Created。这种情况一般出现在用docker create创建了容器但没start或者docker run过程中创建成功但启动失败。它的 STATUS 列会写Created没有时长。遇到这种说明容器的文件系统、网络配置都已经就位就是主进程没跑起来。还有一种更隐蔽的情况容器处于Restarting状态。这时候docker ps是能看到它的但 STATUS 会显示Restarting (1) 3 seconds ago那个数字是重启次数。看到这个基本可以判定应用在启动阶段就崩了陷入了重启循环。这时候别急着重启服务先docker logs --tail 100 容器名看几眼再说。2.3 状态与时间的组合读法STATUS 列的信息密度其实很高我总结了一套快速读法STATUS 显示含义排查方向Up 5 minutes正常运行 5 分钟无异常Up 5 minutes (healthy)正常运行且健康检查通过无异常Up 5 minutes (unhealthy)正常运行但健康检查失败查健康检查脚本与依赖服务Up 5 minutes (health: starting)健康检查还在初始宽限期内稍等再看Restarting (1) 3 seconds ago重启循环已重启 1 次看日志找启动失败原因Exited (0) 2 hours ago正常退出确认是否预期行为Exited (137) 10 minutes ago被 SIGKILL 终止查内存上限、OOMCreated创建未启动手动 start 或检查启动参数Paused被 pause 挂起docker unpause恢复Dead守护进程无法清理需要手工介入删除这张表我贴在过笔记本上现在基本已经是条件反射了。新手最容易犯的错是看到Exited (0)就以为出问题了其实退出码 0 意味着程序自己决定结束可能是任务型容器跑完了属于正常。3. -q、-s、-l、-n那些能改变输出形态的小参数3.1 -q 只输出容器 ID脚本化的起点docker ps -q只打印容器 ID一列不带表头。这个参数本身没什么技术含量但它是几乎所有容器批处理脚本的起点。清理所有已停止的容器docker ps -aq | xargs -r docker rm停掉所有运行中的容器docker ps -q | xargs -r docker stop注意xargs -r里的-r意思是当输入为空时不执行后面的命令。不加这个参数在没有任何容器的时候docker rm会收到一个空参数然后报错。这个细节在很多教程里被省略了但实际写脚本的时候特别重要否则监控脚本半夜告警就是因为这个。还有个更稳的写法是用docker rm $(docker ps -aq)但这种写法在容器数量多的时候可能超出命令行参数长度限制xargs会自动分批处理所以我个人更推荐xargs版本。3.2 -s 显示容器大小磁盘排查的利器docker ps -s会在输出末尾追加两列SIZE 和 virtual size。SIZE 是容器的可写层占用的磁盘空间virtual size 是只读镜像层加可写层的总和。docker ps -s --format table {{.Names}}\t{{.Size}}\t{{.Image}}这个参数平时少用但在磁盘告警的时候极其有用。我遇到过一次根分区快满docker system df显示镜像占了 40G但清完镜像还是紧张最后靠docker ps -a -s才找到几个停止的容器可写层各攒了七八个 G 的日志文件。注意-a和-s要一起用因为吃磁盘的大头往往是那些已经停了但没删的容器。一个小提醒-s需要对每个容器的文件系统做统计容器数量多的时候会有明显延迟。生产机器上几百个容器时这个命令可能要跑好几秒。日常巡检别随手加这个参数只在需要的时候用。3.3 -l 和 -n只看最近的几个docker ps -l显示最后创建的那个容器等价于docker ps -n 1。docker ps -n 5显示最近创建的 5 个容器注意这里说的是创建时间的最近不是运行时间。这个参数在调试 CI 流水线的时候特别好用。构建脚本跑完想知道刚才那个任务容器啥情况直接docker ps -ln 3就能看到最近三个不用在几百个容器里翻。-l和-n都隐含了-a的语义也就是说它们会自动包含已停止的容器这点很多人不知道。3.4 --no-trunc不截断输出默认情况下CONTAINER ID 截断到 12 位COMMAND 列超长会加省略号这个省略号有时候会盖住关键参数。加--no-trunc就完整输出docker ps --no-trunc --format table {{.Names}}\t{{.Command}}这个在比对两个容器启动命令差异时特别有用。曾经有个服务两个副本一个正常一个起不来docker ps看着 COMMAND 差不多加上--no-trunc才发现挂掉的那个被人手动加了--debug参数把日志量打爆了导致启动超时。4. --format 才是 docker ps 的正确打开方式4.1 Go 模板语法的基本用法如果你只会敲裸的docker ps那这个命令的能力你大概只用了三成。--format参数支持 Go 模板语法可以自由挑选、排序、组合字段输出的结果拿去做监控、做报表、做告警都直接可用。最简单的用法是指定单字段docker ps --format {{.Names}}多字段用\t分隔加上table前缀会带上表头docker ps --format table {{.Names}}\t{{.Status}}\t{{.Ports}}table前缀是可选的不加就纯数据输出加了才有对齐的表头行。写脚本解析的时候不加table给人看的时候加上这个习惯建议养成。4.2 可用的字段清单与实战组合常用的占位符有这么一批占位符含义备注.ID容器 ID默认 12 位配合--no-trunc完整.Names容器名注意是复数形式.Image镜像名含标签.Command启动命令默认截断.CreatedAt创建时间绝对时间脚本友好.RunningFor已运行时长相对时间如 3 hours ago.Status状态含健康检查信息.Ports端口映射未映射时为空.State状态词running / exited 等纯状态.Size磁盘占用需配合-s.Labels标签串逗号分隔.Mounts挂载卷名称列表.Networks网络所属网络名.LocalVolumes本地卷数量数字.CreatedAt和.RunningFor的区别值得单独说一句。前者是绝对时间戳格式比如2024-05-12 14:23:01 0800 CST后者是相对描述3 hours ago。人眼看相对时间更直观但脚本做时间比较、做报表归档必须用.CreatedAt因为 3 hours ago 这种字符串根本没法解析成时间。我自己机器上常驻的别名是这个alias dpsdocker ps --format table {{.Names}}\t{{.Status}}\t{{.Ports}}\t{{.Image}}四个字段名字、状态、端口、镜像覆盖了日常九成的查看需求。把镜像放最后是因为它最长放前面会把整行撑宽。4.3 用 format 做轻量监控--format和--filter组合起来能当简易监控用。比如定时检查有没有 unhealthy 的容器docker ps --filter healthunhealthy --format {{.Names}}有输出就说明有容器健康检查挂了配合 cron 或者监控脚本做告警比装一整套监控系统轻量得多。再比如统计各状态容器数量docker ps -a --format {{.State}} | sort | uniq -c | sort -rn一行命令就能看出一台机器上 running、exited、created 各有多少个快速判断这台机器是不是积攒了太多垃圾容器。还有个我常用的找出所有没有映射端口的运行中容器排查为什么访问不到docker ps --format {{.Names}}\t{{.Ports}} | grep -v \-PORTS 列为空或者只有80/tcp这种没有-说明端口没映射到宿主机外部当然访问不了。这个坑新手踩得特别多容器里服务明明起来了外面就是连不上八成是端口没映射。5. --filter 过滤器把范围缩到一个容器5.1 常用过滤器一览docker ps的-f/--filter参数支持按多种维度筛选而且可以叠加多个多个过滤器之间是 AND 关系。过滤器作用示例status按状态筛选statusexitedname按容器名支持部分匹配namewebid按容器 IDida1b2c3label按标签labelenvprodancestor按来源镜像ancestornginx:1.25exited按退出码exited137health按健康状态healthunhealthybefore创建早于某容器beforeweb-proxysince创建晚于某容器sinceweb-proxyvolume挂载了指定卷volumeapp-datanetwork在指定网络中networkbridgepublish映射了指定端口publish8080expose暴露了指定端口expose80name过滤器用的是子串匹配不是精确匹配。--filter nameweb会匹配web-proxy、my-web-app、web所有含 web 的容器。想要精确匹配得用锚定写法--filter name^web-proxy$这个正则在 Docker 里是支持的但文档里写得比较隐晦。5.2 组合过滤排查真实问题说说我实际用过的一组组合。有次一个服务挂了怀疑是重启循环想快速确认最近两小时内退出过的容器有哪些docker ps -a --filter statusexited --format {{.Names}}\t{{.Status}} | grep -E minutes ago|seconds ago更进一步想查哪些容器是因为 OOM 被杀docker ps -a --filter exited137一条命令直接锁定。退出码 137 128 9SIGKILL 的信号号是 9这个换算关系值得记一下。同理 143 128 15对应 SIGTERM。再举一个按标签批量操作的场景。如果启动容器时都打了envprod标签那批量操作就非常干净docker ps -q --filter labelenvprod | xargs -r docker restart比靠名字前缀匹配靠谱得多不用担心误伤。这也是我一直推荐在docker run时加--label的原因成本极低收益是后面所有批量操作都有了准星。5.3 ancestor 过滤器的一个坑ancestor按镜像筛选但它有个不太直觉的行为除了匹配完全相同的镜像名加标签它还会匹配以该镜像为父镜像构建出来的容器。也就是说--filter ancestornginx:1.25可能返回一个基于你自己构建的、FROM nginx:1.25 的镜像启动的容器。这个行为大部分时候是好事能覆盖衍生镜像但如果你要精确统计某个镜像跑了多少容器可能就会多算。还有一个细节是镜像 ID 也能用作 ancestor 的值--filter ancestor$(docker images -q nginx:1.25)在镜像标签频繁变动的环境里用 ID 更稳。6. 那些年踩过的坑输出错位、找不到容器、状态误读6.1 COMMAND 列被截断导致的误判前面提过--no-trunc这里展开说说为什么这个坑很隐蔽。默认截断之后两个启动参数差异很大的容器显示出来的 COMMAND 可能一模一样都是/docker-entrypoint.…。你以为它们配置相同实际参数差了十万八千里。排查步骤应该是先docker ps --no-trunc看完整命令再docker inspect 容器看完整的 Config.Cmd 和 Config.Entrypoint。inspect 输出的是结构化的 JSON比 ps 的字符串拼接准确得多特别是命令里有引号、有空格的时候ps 的显示会让人误判参数个数。6.2 容器看不见的四种情况汇总一下我遇到过的、docker ps找不到容器的场景按出现频率排序第一种容器已退出没加-a。这是最常见的占比可能七成。第二种容器在别的 Docker 上下文里。如果机器上配了多个 context或者用DOCKER_HOST环境变量连了远程守护进程docker ps查的是当前 context 下的容器。用docker context ls看看当前指向哪里这个坑在多人共用一台构建机的环境里特别容易踩。第三种容器在别的命名空间里。用docker compose起的项目容器名会自动加项目前缀比如myapp-web-1用完整名字或者docker compose ps去查更稳。第四种容器被删了。这种情况docker ps -a也不会有得去看docker events的历史记录或者查审计日志。容器被自动清理比如加了--rm参数或者被清理策略扫掉的时候事后是查不到痕迹的。6.3 端口映射那一列的读法PORTS 列的信息量很大但格式对新手不友好。几种典型写法0.0.0.0:8080-80/tcp宿主机的所有网卡的 8080 端口映射到容器的 80 端口127.0.0.1:8080-80/tcp只监听本机回环地址外部访问不到:::8080-80/tcpIPv6 的通配监听80/tcp容器内暴露了 80 端口但没有映射到宿主机空容器没有暴露任何端口最容易误判的是第二种。容器起了端口也映射了docker ps看着有8080-80/tcp但从别的机器就是连不上。原因就是绑定在了127.0.0.1上只有本机能访问。启动命令里-p 127.0.0.1:8080:80和-p 8080:80的差别就在这前者只绑回环后者绑所有网卡。这个细节在安全加固的时候是有意为之但在排查连通性问题的时候经常被忽略。6.4 输出宽度导致的换行错乱终端窄的时候docker ps的表格会折行PORTS 列的内容跑到下一行去看着像多了一条记录。用管道接cat或者column能缓解docker ps | column -t -s $\t不过更根本的解法是用--format只输出你关心的列把输出宽度降下来。我一般不看 COMMAND 和 CREATED 两列省下的宽度让 PORTS 完整显示。7. 把 docker ps 嵌进日常工作流7.1 别名与函数让高频组合一键触发重复敲长命令是效率杀手我机器上的.bashrc里有这么一组alias dpsdocker ps --format table {{.Names}}\t{{.Status}}\t{{.Ports}} alias dpsadocker ps -a --format table {{.Names}}\t{{.Status}}\t{{.Image}} alias dpsqdocker ps -q alias dpsxdocker ps -a --filter statusexited --format {{.Names}}dpsx那个是专门列已退出容器的看多了就会形成习惯——每次服务异常先扫一眼退出列表比翻日志快。如果要更灵活一点可以写个函数按参数切换显示模式dps() { case $1 in a) shift; docker ps -a --format table {{.Names}}\t{{.State}}\t{{.Status}} ;; s) shift; docker ps --format table {{.Names}}\t{{.Size}} ;; h) shift; docker ps --filter healthunhealthy --format {{.Names}} ;; *) docker ps --format table {{.Names}}\t{{.Status}}\t{{.Ports}} ;; esac }这种封装看你个人习惯简单的 alias 其实够用。关键是别把所有参数都背下来常用的三五个组合固定成别名就行。7.2 与其它命令的衔接docker ps很少单独用基本都是链条的一环。几个高频衔接配合docker stats看实时资源占用docker stats $(docker ps -q)docker ps -q在没容器的时候会返回空docker stats收到空参数会报错。稳妥点写成docker stats $(docker ps -q) 2/dev/null || echo 没有运行中的容器按名称批量看日志docker ps --format {{.Names}} | xargs -I{} sh -c echo {} ; docker logs --tail 5 {}这个在排查一组相关联的服务时很有用一次性把每个容器的最后几行日志打出来比一个个docker logs快得多。导出容器清单做资产盘点docker ps -a --format {{.Names}},{{.Image}},{{.CreatedAt}},{{.State}} containers.csv--format支持逗号分隔直接就能生成 CSV导进表格软件里做统计。这个用法我每个月做资源盘点的时候都会跑一次看看有哪些长期停止的容器可以清掉、哪些镜像版本该升级了。7.3 一个容易忘的等价关系docker ps其实等价于docker container lsdocker ps -a等价于docker container ls -a。新版 Docker 在推docker container这层子命令docker ps作为兼容保留着。两者参数完全一致没有任何行为差异。这个知识点看着没什么用但在看官方文档的时候挺重要文档里有时用docker container ls --filter ...举例你要是不知道它是同一个命令可能会以为是新功能。另外写脚本的时候用哪个都行我个人还是习惯docker ps短。还有docker compose ps和docker ps的区别也提一句前者只显示当前 compose 项目下的容器输出格式是 compose 特有的多了 Service、Health、ExitCode 这些列。在多项目共存的机器上排查时用docker compose ps能少看很多干扰项。8. 最后说几个真金白银换来的经验容器的名字是随机生成的默认是形容词_名词的格式比如happy_einstein、zen_hopper。启动时不用--name指定名字事后想找某个容器就只能在几十个随机名里翻。所以我的规矩是只要是有长期存在价值的容器一律手动指定 name而且命名规则固定成项目-服务-序号比如shop-web-01、shop-db-01。用--filter nameshop就能一把捞出来比靠 ID 前缀猜靠谱多了。第二个经验是关于执行时机的。docker ps显示的是某一时刻的快照容器状态变化是很快的。排查崩溃重启类问题的时候连续敲几次docker ps -a看 STATUS 列的时长变化比单次快照信息量大得多。如果两次之间时长归零了说明中间重启过一次重启频率一眼就能看出来。想更精确就看docker inspect里的RestartCount字段是个纯数字比看相对时间靠谱。第三个是清理节奏。已停止的容器不会自己消失每个都在磁盘上占着可写层。我建议把docker ps -a --filter statusexited检查列为周例行看看有没有本该删除却一直留着的。清理的时候注意先确认容器的可写层没有需要保留的数据——容器里没挂卷的写入删容器就没了。要留数据先docker cp出来或者确认它用的是命名卷。最后一个细节--format里的占位符是大小写敏感的.Names能出结果.names会输出空字符串而且不报错。这个静默失败特别坑脚本里写错了字段名跑起来不报错但结果全空排查半天才发现是大小写问题。建议第一次用某个占位符的时候先手动跑一遍确认输出符合预期再写进脚本。