容器跑起来之后,日志就是你的眼睛:服务为什么挂了、请求为什么报错、程序到底有没有按预期在跑,全靠日志说话。但日志也是个双刃剑——不管它,它能悄悄把磁盘写满,让整台机器的服务一起陪葬;管得太死,真出事的时候又发现什么都没留下。这篇把 Docker 日志从「怎么看」到「怎么管」一次讲透:先讲 docker logs 的全参数用法,再讲日志到底存在哪、json-file 驱动是怎么回事,然后是轮转配置的两种写法(全局 daemon.json 和单容器 compose),最后是磁盘写满的应急处理和用 Dozzle 在网页上集中看日志的部署与安全加固。
如果你还没装好 Docker,先看 《如何在Linux部署Docker&Compose》;容器已经在自动更新的,也可以配合 《如何在Linux自动更新Docker容器》 一起看——更新后容器起不来时,第一件事就是查日志。
docker logs 全参数用法
docker logs 是最常用的查日志命令,但很多人只会最基础的 docker logs 容器名,把全部历史日志一股脑刷屏。其实它有一整套参数,组合好了定位问题非常快。
基础与实时跟踪:-f
# 查看容器全部日志(标准输出 + 标准错误)docker logs nginx
# 实时跟踪新日志,类似 tail -f,Ctrl+C 退出(不会停掉容器)docker logs -f nginx-f(--follow)是最常用的参数:先把已有日志打出来,然后挂着等新日志。调试「请求进来之后发生了什么」时,先开一个终端 docker logs -f 挂着,再去发请求,日志实时滚动,问题一目了然。
只看最后 N 行:—tail
# 只看最后 100 行docker logs --tail 100 nginx
# 和 -f 组合:先显示最后 50 行,再实时跟踪docker logs -f --tail 50 nginx容器跑了几个月,日志可能有几十万行,直接 docker logs 会刷屏半天。养成习惯:先 --tail 看尾巴,确认大概是什么问题,再决定要不要翻历史。
按时间范围查:—since / —until
# 看最近 10 分钟的日志docker logs --since 10m nginx
# 看最近 1 小时的日志docker logs --since 1h nginx
# 看某个时间点之后的日志(RFC3339 格式)docker logs --since 2026-11-15T08:00:00 nginx
# 两个组合:看 08:00 到 09:00 这一小时的日志docker logs --since 2026-11-15T08:00:00 --until 2026-11-15T09:00:00 nginx
# 看某个时间点之前的日志docker logs --until 30m nginx--since / --until 接受两种格式:相对时长(10m、1h、24h)和绝对时间戳(RFC3339,如 2026-11-15T08:00:00)。「用户说昨晚 9 点左右网站打不开」这种场景,--since + --until 把时间窗一卡,日志量立刻从几十万行缩到几百行。
显示时间戳:-t
docker logs -t --tail 20 nginx输出会变成这样,每行前面多出日志产生的时间:
2026-11-15T00:12:31.123456789Z 192.168.1.10 - - "GET / HTTP/1.1" 200 612-t(--timestamps)显示的是日志条目的时间戳(UTC 时间,注意换算时区)。应用本身日志不带时间、或者想和别的系统日志对时间线时,这个参数非常有用。
显示驱动附加信息:—details
docker logs --details nginx--details 会把日志驱动附加的额外信息(attributes)一并显示出来,比如通过 docker run --log-opt labels=xxx 打上的标签信息。日常用得不多,排查「这条日志到底从哪个环境来的」时能派上用场。
组合场景速查
| 场景 | 命令 |
|---|---|
| 刚部署完,实时盯启动过程 | docker logs -f --tail 50 容器名 |
| 服务突然挂了,看死前发生了什么 | docker logs --tail 200 容器名 |
| 用户报某个时间段出问题 | docker logs --since 时间 --until 时间 容器名 |
| 日志太多,先粗筛关键词 | docker logs --since 1h 容器名 2>&1 | grep -i error |
| 把日志存下来慢慢看 | docker logs --since 24h 容器名 > app.log 2>&1 |
WARNING
docker logs看到的是容器的标准输出(stdout)和标准错误(stderr)。如果应用把日志写进了容器内部的文件(比如/var/log/app.log),docker logs是看不到的——这也是后面排障小节里最常见的「没输出」原因。
进阶技巧:过滤、统计与导出
参数之外,再配合 shell 的文本工具,docker logs 还能玩出不少花样:
# 只看标准错误(1 是 stdout,2 是 stderr;docker logs 默认两者都输出)docker logs 容器名 2>/dev/null # 只要标准输出docker logs 容器名 1>/dev/null # 只要标准错误
# 统计最近一小时 ERROR 出现了多少次docker logs --since 1h 容器名 2>&1 | grep -c "ERROR"
# 按状态码粗筛访问日志(以 Nginx 为例,看所有 5xx)docker logs --since 24h nginx 2>&1 | grep '" 5'
# 导出某天的完整日志留档docker logs --since 2026-11-14T00:00:00 --until 2026-11-14T23:59:59 容器名 > nginx-1114.log 2>&1这里有个容易踩的小坑:docker logs 输出时,容器写到 stderr 的内容在终端里也是走 stderr 通道的,所以管道给 grep 之前记得加 2>&1,否则错误日志会漏掉一大半。很多应用恰恰是把正常日志写 stdout、把错误日志写 stderr,只过滤一半等于白查。
容器日志到底存在哪
理解日志的存放位置,才能理解轮转和磁盘占用的问题出在哪。
在默认的 json-file 日志驱动下,每个容器的日志都存在宿主机上一个固定的文件里:
/var/lib/docker/containers/<容器ID>/<容器ID>-json.log查看某个容器日志文件的实际大小:
# 先拿到容器完整 IDdocker inspect -f '{{.Id}}' nginx
# 再看对应日志文件ls -lh /var/lib/docker/containers/<容器ID>/<容器ID>-json.log也可以一条命令直接定位:
docker inspect -f '{{.LogPath}}' nginxjson-file 驱动的工作原理很朴素:Docker 守护进程把容器的 stdout/stderr 一行一行包装成 JSON(每行包含日志内容、来源是 stdout 还是 stderr、时间戳),追加写进这个文件。docker logs 本质上就是把这个文件读出来、拆掉 JSON 包装再打印。
朴素的代价是:默认不做任何轮转。只要容器还在输出,这个文件就只会变大,永远不会自己变小。一个话痨容器(比如开了 debug 级别日志的服务)几天写出几十 GB 日志,是非常真实的事故——这正是下一节要解决的问题。
日志轮转:全局配置 daemon.json
给所有容器统一设置日志上限,改的是 Docker 守护进程的配置文件 /etc/docker/daemon.json:
{ "log-driver": "json-file", "log-opts": { "max-size": "20m", "max-file": "3" }}两个参数的含义:
max-size: 20m:单个日志文件最大 20MB,写满了就滚动到下一个文件;max-file: 3:最多保留 3 个文件(当前文件 + 2 个历史文件)。
也就是说每个容器最多占用约 60MB 磁盘,写得再多也封顶,老日志自动丢弃。
改完之后要重启 Docker 守护进程才生效:
systemctl restart docker验证配置是否被守护进程接受(改错 JSON 语法会导致 Docker 起不来,重启后先确认服务状态):
systemctl status dockerdocker info | grep -A2 "Logging Driver"WARNING最关键的坑:daemon.json 的日志配置只对「新建的容器」生效。 已经在跑的老容器,创建时用的是旧配置,重启 Docker、甚至重启容器本身都不会改变它的日志设置——必须把容器删除重建(
docker compose up -d --force-recreate或先docker rm再重新创建),新配置才套得上。所以改完 daemon.json 别急着以为万事大吉,逐个确认重要容器的实际配置:Terminal window docker inspect -f '{{.HostConfig.LogConfig}}' 容器名输出里能看到该容器实际生效的
max-size和max-file,没有就说明它还是无限制状态。另外提醒一句:systemctl restart docker会影响所有容器,没有配live-restore的话容器会被全部重启,请挑低峰期操作。
日志轮转:单容器配置(Compose)
全局配置管所有容器,但有些容器你想区别对待:核心服务想多留点日志(max-file 给大些),一次性的小工具干脆少留。这时在 docker-compose.yml 里给单个服务写 logging 段,它会覆盖全局 daemon.json 的设置:
services: app: image: myapp:latest logging: driver: json-file options: max-size: "50m" max-file: "5"这个服务就会按单文件 50MB、保留 5 个来轮转,总上限约 250MB,不受全局 20m×3 的影响。
用 docker run 时对应的是 --log-opt 参数,效果一样:
docker run -d \ --log-driver json-file \ --log-opt max-size=20m \ --log-opt max-file=3 \ nginx两种写法的分工很简单:daemon.json 定基线,保证任何容器都不会写爆磁盘;compose 的 logging 段做个别调整,给需要特殊对待的服务开小灶。
轮转参数的几个细节
- 单位:
max-size支持k(KB)、m(MB)、g(GB),比如500k、20m、1g都合法,不带单位默认是字节,千万别手滑写成纯数字。 - 到底占多少:一个容器的日志上限约等于
max-size × max-file。20m × 3 = 60MB,单机跑二三十个容器,最坏情况也就 2GB 以内,心里要有这本账;如果某个服务日志量特别大又想多留历史,把它单独调大,而不是把全局基线放宽。 - 轮转不是删除:写满一个文件后,旧文件会被改名留作历史(如
-json.log.1、-json.log.2),docker logs读取时会自动把这几个文件拼起来看,所以轮转不会让近期日志「断片」,只是更早的会被挤掉。 - 正在写的容器能改吗:不能热改。容器的日志配置在创建那一刻就定死了,任何修改——不管是改 daemon.json 还是改 compose 文件——都必须重建容器才生效,这一点前面已经强调过,这里再重复一遍,因为它是本篇最容易翻车的地方。
其他日志驱动对比
json-file 不是唯一选择。Docker 还内置了几种日志驱动,适用场景各不相同:
| 驱动 | 日志去向 | docker logs 能看吗 | 适用场景 |
|---|---|---|---|
json-file | 宿主机本地 JSON 文件 | ✅ 能 | 默认驱动,单机小规模部署,配好轮转就够用 |
local | 宿主机本地文件(Docker 优化格式) | ✅ 能 | json-file 的改良版:自带轮转和压缩,磁盘占用更小,官方推荐的新选择 |
journald | 宿主机的 systemd journal | ✅ 能(经 journal 读取) | 宿主机用 systemd 且已有 journal 收集习惯,想和系统日志统一管理 |
syslog | 宿主机的 syslog 服务 | ❌ 不能 | 已有 syslog/远程日志服务器的老牌环境 |
none | 直接丢弃 | ❌ 不能 | 输出量巨大且完全不需要日志的容器,或日志已由应用自己处理 |
几点说明:
local驱动值得多看一眼:它就是为解决 json-file 的毛病设计的,默认就带轮转(max-size默认 20m、max-file默认 5),还做了压缩和更高效的读写。新部署的机器可以直接把 daemon.json 的log-driver换成local,省去手动配轮转。journald的好处是能按容器名过滤查看,坏处是日志和系统日志混在一起,且 journal 本身也有自己的轮转策略要管。用了 journald 之后,除了docker logs,还可以直接在宿主机上按容器名查:
journalctl CONTAINER_NAME=nginx -flocal驱动多提一句:它的日志文件不在前面说的-json.log路径下,格式也是二进制优化过的,不能直接用文本工具去翻文件,老老实实用docker logs看就行。它省下的主要是磁盘 I/O 和空间,对日志量大的机器收益明显。syslog驱动下docker logs完全失效,日志只在 syslog 里,换机器排障时容易两眼一抹黑。
WARNING
none驱动是个大坑预警:设成none后容器日志被直接丢弃,docker logs执行不报错、但什么都输出不了。哪天你发现某个容器「日志功能坏了」,先用docker inspect -f '{{.HostConfig.LogConfig.Type}}' 容器名查一下它到底用的什么驱动——十有八九是创建时被设成了none,或者是从别人那抄的 compose 文件里带着这个设置。
磁盘被日志写满:排障全流程
这是 Docker 新手最容易吃的一次亏:服务器突然各种服务报错、数据库连不上、SSH 登录都变慢,最后发现是根分区 100% 满了,而凶手往往就是某个容器的日志。按这个流程走,十分钟能定位并止血。
第一步:确认磁盘真的满了,满在哪
# 看分区使用率,确认哪个分区满了(一般是 / 或 /var)df -h
# 看 /var/lib/docker 占了多少du -sh /var/lib/docker第二步:定位大文件
# 按容器列出占用,排序找最大的du -sh /var/lib/docker/containers/* | sort -rh | head -20
# 或者直接找所有超过 100MB 的日志文件find /var/lib/docker -name "*-json.log" -size +100M -exec ls -lh {} \;一般到这一步凶手就现形了:某个 -json.log 文件几个 GB 甚至几十 GB。
想知道它属于哪个容器,用文件路径里的容器 ID 反查:
docker ps -a --no-trunc | grep <容器ID前几位>第三步:应急清空日志(truncate,不能 rm)
确认这个日志不需要保留后(或者先 tail 拷一份样本出来),用 truncate 把文件清零:
truncate -s 0 /var/lib/docker/containers/<容器ID>/<容器ID>-json.log或者等价写法:
cat /dev/null > /var/lib/docker/containers/<容器ID>/<容器ID>-json.logWARNING千万不要直接
rm删这个日志文件! 容器进程和 Docker 守护进程还握着这个文件的句柄,rm只是删掉了目录项,进程会继续往那个「已经不存在」的文件里写,磁盘空间一点都不会释放(用lsof | grep deleted能看到它还占着),直到容器重启句柄才释放。更糟的是,新日志从此写进黑洞,docker logs也再看不到新内容。truncate -s 0是把同一个文件清零,句柄不变,空间立刻释放,容器继续正常写日志——这才是正确姿势。
多说一句怎么判断「凶手是不是日志」:磁盘写满时报错往往很迷惑——数据库可能报连接失败或只读错误,Web 服务返回 502,甚至 Docker 本身拉镜像、建容器都会失败,因为这些操作全都要往磁盘写东西。如果一台机器突然「所有服务一起出问题」,第一反应就应该是去看 df -h,而不是逐个服务去猜。反过来,单个服务出问题、磁盘还很空,那就老老实实查应用本身,别在磁盘上浪费时间。
清完后再 df -h 确认空间回来,服务一般会自己恢复;个别已经崩掉的容器手动重启一下:
docker restart 容器名第四步:事后补上轮转配置
止血只是第一步,不补配置,过几天同样的事情还会再发生一遍:
- 按前文给 daemon.json 加上
log-opts(max-size 20m、max-file 3); - 重启 Docker 让配置生效;
- 把出事的那个容器删除重建(记住:只对新建容器生效),确认新容器的
LogConfig里已经带上轮转参数; - 顺手检查其他老容器,逐个重建或者在 compose 里补
logging段。
加一道保险:定期巡检大日志
轮转配置难免有漏网之鱼(比如别人部署的容器、临时测试起的容器),可以再加一个简单的巡检脚本,定期扫一遍超大日志文件,提前发现苗头:
#!/bin/bash# /opt/scripts/check-docker-logs.sh:找出超过 500MB 的容器日志并记录find /var/lib/docker/containers -name "*-json.log" -size +500M \ -exec ls -lh {} \; >> /var/log/docker-logs-check.log 2>&1挂到 crontab 每周跑一次,或者配合你已有的监控,在文件出现时就告警:
0 9 * * 1 /opt/scripts/check-docker-logs.sh思路和自动更新一样:重复性的检查交给定时任务,人才有精力处理真正需要判断的问题。等哪天巡检日志里真出现了一行记录,说明又有容器在偷偷攒日志,按前面的流程处理即可。
Dozzle:网页端集中看日志
容器一多,SSH 上去一个个 docker logs 就很折腾了。Dozzle 是一个轻量的日志查看 Web 界面:实时流式显示所有容器的日志、支持搜索过滤、多容器一键切换,镜像本身只有十几 MB,是个人服务器上最顺手的日志工具。
Compose 部署
services: dozzle: image: amir20/dozzle:latest container_name: dozzle restart: unless-stopped environment: - TZ=Asia/Shanghai volumes: - /var/run/docker.sock:/var/run/docker.sock:ro ports: - "127.0.0.1:8080:8080"docker compose up -d注意端口写法是 127.0.0.1:8080:8080——只绑定本机回环地址,外网直接访问不到,原因在下一节细说。
界面能干什么
- 实时日志流:打开就是所有容器的列表,点进任意容器,日志像
docker logs -f一样实时滚动,不用手动刷新; - 多容器切换:左侧列表一键切换,还能同屏对比看几个容器的日志,排查「A 服务调 B 服务失败」这类跨容器问题特别方便;
- 搜索与过滤:在日志流里直接关键词搜索、高亮命中行,比在终端里翻
grep直观得多; - 历史回看:往上滚动就能加载更早的日志,带时间戳显示;
- 几乎零配置:它直接读 Docker 的日志,你现有的容器什么都不用改,装上就能看。
Dozzle 只是「看」日志的工具,它不存储、不转储日志——容器删了,日志在 Dozzle 里也就没了,这一点和 docker logs 的行为一致。
再介绍两个实用的可选项。第一,只展示部分容器:机器上容器多了,列表会很长,可以用过滤参数只盯你关心的那些,过滤语法和 docker ps --filter 一致:
command: --filter label=com.centurylinklabs.watchtower.enable=true或者按名字过滤:
command: --filter name=nginx --filter name=app第二,挂在反向代理的子路径下:如果想把 Dozzle 放在 https://你的域名/logs/ 这种子路径而不是独立端口/子域名,给它设置基础路径:
environment: - DOZZLE_BASE=/logs反向代理把 /logs/ 转发到 Dozzle 的 8080 端口即可,页面里的静态资源路径会自动带上这个前缀,不会出现页面打开了但样式和脚本 404 的问题。
安全加固(重中之重)
Dozzle 好用,但它的能力来源是那块 /var/run/docker.sock,这也是它最大的风险点,必须认真对待。
先说清楚 docker.sock 是什么:它是 Docker 守护进程的控制接口,谁能访问它,谁就能创建任意容器、挂载宿主机任意目录、以 root 权限在宿主机上执行命令——等于宿主机的 root 权限。Dozzle 挂的是只读(:ro),这能防止通过 Dozzle 本身修改容器状态,是必须保留的设置;但即便如此,能看所有容器的全部日志本身就很敏感:日志里经常混着数据库密码、API Token、用户隐私数据。
所以部署时请务必做到这三点:
- 不要直接暴露公网。 端口只绑
127.0.0.1(像上面 compose 里那样),然后通过反向代理(Nginx/Caddy)加 HTTPS 访问,或者干脆只在内网/通过 SSH 隧道访问:
# 在自己电脑上开隧道,然后浏览器访问 http://localhost:8080ssh -L 8080:127.0.0.1:8080 root@你的服务器- 开启 Dozzle 自带的登录认证。 给容器加两个环境变量即可启用用户名密码登录:
environment: - TZ=Asia/Shanghai - DOZZLE_USERNAME=admin - DOZZLE_PASSWORD=换成你的强密码- 反向代理再加一层。 如果确实需要从公网访问,在 Nginx 层再加 Basic Auth 或 IP 白名单,和 Dozzle 自带认证叠加,双保险。
WARNING把 Dozzle 裸奔在公网上(
8080:8080不绑回环、不设密码)是真实世界里被入侵的常见入口之一:扫描器全天候在扫这个端口,一旦被人打开,你所有容器的日志——包括里面可能出现的密钥和隐私——都一览无余,对方还能从日志里摸清你的全部服务结构。花在加固上的这三分钟,绝对不能省。
排障:docker logs 没输出怎么办
docker logs 执行成功、退出码正常,但就是一片空白。按可能性从高到低排查:
1. 应用把日志写进了文件,没走 stdout/stderr。
这是最常见的原因。docker logs 只能拿到容器主进程的标准输出和标准错误;很多传统应用默认把日志写到 /var/log/xxx.log 这类文件里,自然看不到。验证方法:进容器里看看有没有日志文件在长大:
docker exec -it 容器名 sh -c 'ls -lt /var/log/ | head'解决办法是改应用的日志配置让它输出到 stdout;实在改不了,可以在启动命令里把日志文件软链到标准输出,这也是很多官方镜像的做法:
RUN ln -sf /dev/stdout /var/log/app.log \ && ln -sf /dev/stderr /var/log/app-error.log2. 日志驱动被设成了 none。
前面提过,none 驱动直接丢弃日志。检查并重建容器换回 json-file 或 local:
docker inspect -f '{{.HostConfig.LogConfig.Type}}' 容器名3. 容器已经删除了。
日志文件是跟着容器走的,容器被 docker rm(或者被自动更新工具重建失败后清掉)之后,日志文件一并删除,事后无从查起。这也是为什么 《如何在Linux自动更新Docker容器》 里建议重要服务先开「只监控」观察期、更新前先看日志:真出了事,旧容器的日志还在才有据可查。需要长期留存的日志,应该让应用输出到挂载出来的数据卷文件里,或者接入集中日志系统,而不是只依赖 docker logs。
4. 用了 —since 却把时间算错了。
不加参数明明有日志,一加 --since 就空白,多半是时间对不上:docker logs 的绝对时间按 UTC 理解,而你脑子里想的是北京时间,差着 8 小时,窗口自然卡空了。不确定时先用相对时长(--since 30m)验证,或者用 date -u 看一眼服务器的 UTC 时间再换算。
5. 顺带一提:输出被缓冲了。
有些语言(典型如 Python)的 stdout 在非终端环境下是块缓冲,程序在跑、日志却攒着不吐出来,看起来像没输出,过一阵又突然刷出一大片。这不是 Docker 的问题:Python 加 -u 参数或环境变量 PYTHONUNBUFFERED=1 即可,其他语言同理查各自的无缓冲设置。
再往后:什么时候需要集中日志系统
本篇讲的都是单机范围内的日志管理:一台机器、几十个容器,docker logs 加 Dozzle 完全够用。但如果你的情况变成下面这样,就该考虑把日志集中收集到专门的系统里了:
- 机器变多:三五台服务器各跑各的容器,出一次问题要挨个 SSH 上去翻日志,效率太低;
- 日志要长期留存:审计、复盘需要查几个月前的日志,而本机轮转早就把它们挤掉了;
- 需要告警:想在日志出现特定错误(比如支付失败、异常堆栈)时第一时间收到通知,而不是等用户来报障。
常见的集中方案有 Loki + Promtail + Grafana(轻量,和监控面板能合一)、ELK(Elasticsearch + Logstash + Kibana,功能强但吃资源)等。它们的共同思路是:在每台机器上跑一个采集小组件,把容器日志实时转发到中心存储。个人规模建议从 Loki 起步,资源占用小、和 Docker 的配合也成熟。不过记住一句话:集中系统是锦上添花,单机的轮转配置永远是地基——采集组件挂掉的时候,本机日志如果没封顶,照样能把磁盘写满,地基的活儿一天都不能省。
小结
docker logs的日常组合是-f跟踪、--tail限量、--since/--until卡时间窗、-t带时间戳,先缩小范围再细看;- 默认 json-file 驱动不轮转,这是磁盘写满事故的头号来源,必须配轮转:daemon.json 全局设
max-size: 20m+max-file: 3定基线,compose 的logging段给个别容器开小灶; - 牢记 daemon.json 改完只对新建容器生效,老容器要重建,并用
docker inspect逐个验证; - 磁盘真被写满时,流程是
df/du/find定位 →truncate -s 0清零止血(绝不能直接rm)→ 补轮转配置防复发; - 容器多了用 Dozzle 在网页上集中看日志,但 docker.sock 事关宿主机 root 权限:只读挂载、端口绑 127.0.0.1、开登录认证,三件套缺一不可;
docker logs没输出,先怀疑应用没往 stdout 写,其次查日志驱动是不是none,最后确认容器是不是已经被删了。
日志管理的核心就一句话:平时给它封顶,出事时靠它查命。轮转配置花十分钟一次配好,往后每次排障都会感谢现在的自己。