本节摘要:日志是排查的最后裁判。本节从日志的源头讲起——容器 stdout 与 stderr 如何被 json-file 驱动落盘,再到 docker compose logs 的 -f、--tail、按服务过滤三种用法,然后逐行解读连接被拒、找不到文件、权限拒绝、容器反复重启四类高频报错,最后用一个跨容器请求的完整时间线示例,演示如何把零散日志拼成破案链条。读完本节,你能用证据而不是猜测定根因。
排查之前先弄清日志是怎么产生的。Docker 默认日志驱动是 json-file,它把容器内进程写到 stdout 和 stderr 的内容逐行包装成 JSON 记录,存放在宿主机的 /var/lib/docker/containers/容器ID/ 目录下,每个容器一个以 -json.log 结尾的文件。docker logs 命令的本质,就是读取这个文件。
两个推论值得记住。第一,容器删除后日志文件随之删除,想保留现场证据,要么提前导出,要么配置集中收集。第二,磁盘会被日志撑爆——应用写日志不设上限,json-file 文件无限增长,所以生产环境基本都要配滚动策略。
给一份带日志配置的完整 compose 示例:
services: web: image: nginx:1.27-alpine ports: - "8080:80" logging: driver: json-file options: max-size: "10m" max-file: "3" healthcheck: test: ["CMD-SHELL", "wget -q -O - http://localhost || exit 1"] interval: 30s timeout: 10s retries: 3 db: image: mysql:8.4 environment: MYSQL_ROOT_PASSWORD: example volumes: - dbdata:/var/lib/mysql volumes: dbdata:
关键行解读:logging 段的 max-size 表示单个日志文件超过 10MB 就滚动,max-file 表示保留 3 个滚动文件,两者配合把日志占用锁死在 30MB 以内;健康检查用 wget 是因为 alpine 镜像没有 curl,这和第 5.2 节提到的坑对应。规模再大的应用,可以把 driver 换成 syslog、fluentd,把日志送往集中平台。
docker compose logs 是看日志最直接的入口,它把项目内所有容器的日志合并输出,每行带服务名前缀。三种用法按需求组合:
组合示例:docker compose logs -f --tail 50 web,只跟踪 web 服务最近的 50 行。还有两个实用参数:--since 30m 只看最近 30 分钟的日志,--timestamps 显示完整时间戳,跨容器比对时间线时必开。
| 参数 | 作用 | 示例 |
|---|---|---|
| -f | 持续跟踪新日志 | docker compose logs -f |
| --tail 行数 | 只看最后 N 行 | docker compose logs --tail 200 |
| 服务名 | 只显示指定服务 | docker compose logs db |
| --since 时长 | 按时间范围过滤 | docker compose logs --since 30m |
| --timestamps | 显示完整时间戳 | docker compose logs --timestamps |
输出前缀有个版本差异:新版 compose 的服务前缀是 web-1,老版本是 web_1,下划线变连字符,看日志时别认错。
日志级别代表信息的重要程度,级别划分直接决定你从哪行开始读:
| 级别 | 含义 | 处理建议 |
|---|---|---|
| DEBUG | 调试细节,定位问题的最细粒度 | 开发环境用,生产关闭 |
| INFO | 常规运行信息,如启动、请求处理 | 保留,作为正常基线 |
| WARNING | 潜在隐患,不影响当前功能 | 关注但不立即处理 |
| ERROR | 发生错误,功能可能受损 | 立即排查 |
| FATAL | 致命错误,应用无法继续 | 最高优先,先处理它 |
分析顺序有讲究:先找 ERROR 和 FATAL,锁定问题发生的时刻;再往回看同时间的 WARNING 和 INFO,补全上下文——错误本身只告诉你"坏了",上下文才告诉你"怎么坏的"。生产环境日志级别通常设在 INFO 或 WARNING,DEBUG 全开会导致日志量暴涨,反而淹没有效信息。
日志里出现频率最高的四类报错行,逐个拆开看:
连接被拒。典型文本:web_1 | 2023-10-27 10:00:00 ERROR: Could not connect to database: Connection refused。含义是 TCP 连接被目标拒绝,目标端口没有服务在监听。排查顺序:目标服务是否在运行、目标端口是否监听、网络是否可达。如果目标是数据库容器,先 docker compose ps 看它是否还活着。
找不到文件。报错通常是 No such file or directory,带具体路径。先分清路径在容器内还是宿主机:容器内路径报错,查工作目录和挂载是否生效;挂载路径报错,回到第 5.2 节的卷排查流程,docker inspect 看 Mounts 段核实。
权限拒绝。Permission denied 几乎总是 UID 不匹配:容器内进程用户和挂载目录属主不一致。解法是把宿主机目录属主改成容器内用户 UID,chown 之后重启容器。
容器反复重启。日志里看不到应用报错,容器却在疯狂重启,先看退出码:137 是内存被杀死,127 是启动命令不存在,1 是应用自身异常。配合 docker inspect 看 OOMKilled 字段,能直接区分内存问题和代码问题。
⚠️ 常见坑:容器反复重启时,compose logs 输出的是容器最后一次退出的日志,不一定代表根因。先 docker inspect 看 RestartCount 和 OOMKilled 两个字段,再回来看日志,能少走一半弯路。
单看一个容器的日志容易误判,把相关容器的日志按时间戳排在一起,链条就出来了。以"用户访问页面返回 502"为例,nginx、web、数据库三个容器各留一段日志:
10:00:00.100 nginx | 10.0.0.5 GET /api/orders 502 10:00:00.105 web | ERROR: connect ECONNREFUSED 172.20.0.3:3306 10:00:00.102 db | 无任何日志输出
把三行按时间排序后,结论非常清晰:web 在 10:00:00.105 报 3306 端口连接被拒,而数据库容器在这个时刻没有任何日志——请求根本没到达数据库进程。问题不在 SQL 或数据,而在网络层或数据库监听。如果数据库侧有日志,则要把排查重心移到数据库自身。
再对比一次正常请求的时间线:
10:00:01.000 nginx | GET /api/orders 200 10:00:01.008 db | 3 rows returned in 8ms 10:00:01.010 web | INFO: Request processed in 1200ms
数据库 8 毫秒就返回了,web 却花了 1200 毫秒处理——慢在应用逻辑或连接池,不在数据库。日志时间线不但能定位"断在哪",还能定位"慢在哪"。请求量大的系统,日志里加请求 ID,跨容器用请求 ID 对齐日志,比靠时间戳猜精确得多。
💡 关键直觉:日志"没出现"也是一种信息。某个容器在时间线上静默,说明请求根本没到它这一层——这常常直接锁定了断点位置,比看到报错更省事。
这张时序图就是"日志时间线"的可视化版本。正常路径和异常路径一目了然:异常时第一个报错的是 web 到数据库这一段,取证时从数据库容器日志开始核对,能最快收口。
需要提醒的是,跨容器时间线依赖各容器时钟一致。单机部署时所有容器共享宿主机时钟,时间戳天然对齐;跨主机部署时如果各节点时间不同步,先校准时钟再比对,否则时间线会把排查方向带偏。
日志量大之后,靠肉眼扫不现实。三个命令行工具是基本盘:
一条实战管道把三个工具串起来:docker compose logs --timestamps web | grep ERROR | awk '{print $1, $2, $0}'。输出里第一列是时间戳,第二列是服务前缀,后面是完整报错。错误行超过几十条时,把 grep 的关键字换成 Connection refused 或 No such file,范围立刻缩小到真正需要看的内容。
结构化日志是时间线分析的加速器。一条 JSON 格式的日志长这样:
{"ts":"2023-10-27T10:00:00Z","level":"ERROR","service":"web","request_id":"a1b2c3","msg":"connect failed","target":"db:3306"}
request_id 字段就是跨容器对齐的关键:同一请求在每个容器日志里带同一个 ID,grep 一下 request_id 就能把整条链路从 nginx 到数据库全部拉出来,比靠时间戳猜精确一个量级。这也是生产环境强烈建议结构化日志的原因。
单机部署,docker compose logs 完全够用。多机或微服务规模,就要上集中收集:ELK Stack 三件套分工明确,Logstash 负责收集处理,Elasticsearch 负责存储索引,Kibana 负责可视化搜索;Graylog 自带 Web 界面和告警;Fluentd 以插件多著称,适合异构环境;轻量方案选 Promtail 加 Loki,和 Prometheus 监控生态集成顺滑。如果团队已经在用 Prometheus 做监控,Promtail 加 Loki 能复用同一套标签体系,少维护一套独立部署。选型的参考线很简单:一台机器用命令,三台机器上 Loki,规模再大上 ELK。

这张泳道图把日志分析拆成四个阶段:先收集齐证据,再过滤缩小范围,然后跨容器关联时间线,最后对照错误清单定位根因。前两步处理的是"量",后两步处理的是"序"——时间顺序对了,根因自己会浮出来。
排查效率的一半来自平时的日志质量,六个习惯直接见效:
到这里,本章的排查链条就完整了:5.1 的工具、5.2 的清单、5.3 的证据,三件套齐活。下次再遇到"容器又挂了"的消息,先分类,再对表,最后用日志时间线收口——这套流程走熟了,故障处理就不再是熬人的苦差事。