服务器磁盘莫名其妙满,最后查出来是容器日志——这件事几乎每个用 Docker 的站长都遇到过。宿主机上 du -sh /var/lib/docker 一看两个多 G,进去一查,某个容器的 *-json.log 单文件就有 1.8G。更气人的是,你配了 logrotate 却完全没管它,因为 Docker 的日志根本不走 syslog,也不在 /var/log 下面,logrotate 再怎么写都轮不到它。
这篇文章讲清楚三件事:Docker 默认的日志驱动到底把日志写在哪、轮转该怎么配(包括已经涨到 1.8G 的存量文件怎么安全处理)、以及容器日志和宿主机系统日志该怎么分层管理。
一、先搞清楚日志写在哪:json-file 驱动与它的目录约定
Docker 默认的日志驱动是 json-file,每个容器一行 JSON 记录,写在宿主机的固定路径下:
/var/lib/docker/containers/<容器ID>/<容器ID>-json.log这个路径和容器名无关,和 compose 服务名也无关,只跟容器 ID 有关。想知道某个容器的日志到底落在哪个文件、有多大,一条命令就够:
docker inspect --format '{{.LogPath}}' my-app
# /var/lib/docker/containers/3f2a.../3f2a...-json.log
docker ps -q | while read id; do
p=$(docker inspect --format '{{.LogPath}}' "$id")
[ -f "$p" ] && printf '%s\t%s\t%s\n' \
"$(du -h "$p" | cut -f1)" "$(docker inspect --format '{{.Name}}' "$id")" "$p"
done | sort -rh | head第二段脚本是我最常用的巡检命令:列出所有运行中容器的日志文件大小,按大小倒序。第一次跑的时候要有个心理准备,很多人会在这里发现一个占了几 G 的文件。
顺便说清一个高频误解:docker logs 读的就是这个 json.log 文件,它不是在内存里缓存的。所以文件被清空或轮转之后,docker logs 看到的内容也会跟着变化——这一点和「日志已持久化到别处」的直觉不一样。
二、轮转的三种配法,以及它们的适用边界
Docker 的日志轮转是在日志驱动层面做的,配置项只有三个,写在 daemon.json 里:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "50m",
"max-file": "5"
}
}这三个值的意思分别是:单文件上限 50MB,最多保留 5 个文件,也就是一个容器最多占 250MB。注意 max-file 是「文件个数」不是「轮转次数」,写 1 的效果是永不轮转(只有一个文件可用,轮转后没有落点),而不是「只保留一份」。这个语义坑在文档里写得含糊,我在一次排查中就因为它踩了半天。
这套全局配置有两个明确的边界。
第一个边界:只对之后新建的容器生效。已经存在的容器不会因为改了 daemon.json 就变小,也不会开始轮转——因为日志参数是容器的创建时属性,固化在容器配置里。要让存量容器生效,只能重建容器(docker rm 再 docker run,或者 compose 的 up -d --force-recreate)。
第二个边界:改 daemon.json 需要重启 Docker 守护进程,而重启守护进程默认不会重启设了 restart: always 之外的容器——实际上各个发行版行为不一致,有的会重启全部容器,有的不会。稳妥做法是选在维护窗口做,并且事先确认每个容器都能独立启动。
如果不想动全局配置,可以按容器单独配。compose 里的写法是:
services:
app:
image: myapp:latest
logging:
driver: json-file
options:
max-size: "20m"
max-file: "3"按容器配的好处是可以给不同角色定不同额度:应用容器日志多,给 50MB×5;Nginx 容器有自己的 access log 走 stdout,给 20MB×3;数据库容器通常日志很少,给 10MB×2 就够。混在一起用全局配置,等于按最高需求给所有容器都留了空间。
三、存量日志怎么清:三种方法,只有一种安全
配置改好了,磁盘上那 1.8G 还在。这时候有几种做法,风险完全不同。
第一种:直接 rm。
rm /var/lib/docker/containers/<id>/<id>-json.log不推荐。这个文件是容器的打开文件句柄,删掉之后进程还在往里写,内核会把数据写到已被删除的 inode 上——df 显示磁盘没释放,du 找不到对应文件,这就是经典的「删了日志磁盘还是满的」。用 lsof | grep deleted 能看到那个残留句柄,只有重启容器才会释放。
第二种:truncate 清空。
truncate -s 0 /var/lib/docker/containers/<id>/<id>-json.log这是安全的。截断保留 inode,文件句柄继续有效,正在写的进程不受影响,磁盘空间立刻释放。唯一的副作用是日志内容全没了——而这通常正是你要的效果(前提是你不打算事后追溯)。
第三种:让 Docker 自己轮转。
严格说 Docker 没有「立即轮转」的命令。docker system prune 不会动日志(它清的是未使用的镜像、容器、网络、构建缓存,日志不在其列)。真正让文件轮转的唯一方式是达到 max-size 阈值,或者重建容器。所以在配置生效之前,实务顺序只能是:先 truncate 清空存量,再重建容器让新配置生效。
顺便提醒一个更常见的误操作:看到 docker system df 显示很大的数字,就执行 docker system prune -a。这会删掉所有未被运行中容器引用的镜像,包括你打算回滚用的旧版本。日志文件和它没有任何关系,但很多人把它当成「清空间」的万能命令,结果回滚时发现镜像没了。
四、容器日志与宿主机日志的分层关系
最容易被忽视的一点是:Docker 日志和系统日志是两套体系,需要分两层管理。
第一层是映射到宿主机的日志。如果容器里跑的是 Nginx,通常会把 access.log 和 error.log 通过 volume 挂到宿主机目录:
volumes:
- ./logs/nginx:/var/log/nginx这些文件在宿主机上、容器外,logrotate 完全能管,用标准的 /etc/logrotate.d/nginx 配置就行。但这里有个必须注意的顺序问题:logrotate 轮转之后,Nginx 仍然握着旧文件句柄,必须让它重新打开日志文件,否则日志继续写进 access.log.1:
/srv/site/logs/nginx/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
sharedscripts
postrotate
docker kill -s USR1 nginx 2>/dev/null || true
endscript
}docker kill -s USR1 就是给容器内的 Nginx 进程发 HUP/USR1 信号,让它重新打开日志文件。这比 docker exec nginx nginx -s reopen 轻量,不需要 exec 新进程,也不会因为容器里没有 nginx 命令而失败。
第二层是没挂载出来的应用日志。这些日志走 stdout,被 json-file 驱动接走,写在 /var/lib/docker/containers/ 下,只能通过 max-size/max-file 控制。这一层没有任何外部工具能介入(除非换成别的日志驱动),所以配置必须在一开始就做对。
两层之间的判断依据很简单:日志在容器里是否写到了挂载目录。写了就归 logrotate 管,没写(输出到终端)就归 Docker 的 max-size 管。很多站点出问题,就是因为一半日志在挂载目录、一半在 json-file,只配了其中一层。
五、换日志驱动:什么情况下值得,什么情况下不必
json-file 之外还有几种驱动,值得简单说一下取舍。
local 驱动是 json-file 的改良版,同样是本地文件,但内部用了更紧凑的二进制格式,并且默认就带轮转(每个文件 20MB、保留 5 个)。如果你不需要用 docker logs 之外的任何工具读日志,也没有集中收集的需求,local 是更省心的默认值。代价是日志文件不能再用 json-file 的格式解析——任何依赖 JSON 逐行解析的第三方脚本都会失效。
syslog、journald 驱动把容器日志转给宿主机日志体系。journald 在 systemd 发行版上比较自然,优点是通过 journalctl CONTAINER_NAME=xxx 统一查,轮转由 journald 自己的 SystemMaxUse 管,不用再加一层配置。缺点是 journald 一旦写满,所有容器的日志会一起丢,而且 journald 的默认压缩策略在高并发下会有可见的 CPU 开销。
fluentd、loki 这类驱动适合有集中日志平台的多机场景。单机站用它们属于过度工程——引入一个日志收集服务本身就要占几百 MB 内存,为了省几百 MB 日志空间不划算。
我的选择是:单机小站用 json-file + max-size,因为有 docker logs 的便利;容器数量超过十个、或者开始有多台机器,换 local 或 journald。判断标准不是「哪个更先进」,而是「你有没有超过一台机器」。
六、运维巡检:三个数字每周看一次
配好之后不代表不用管。每周花两分钟看三个数字就能提前发现问题。
第一个是日志文件总占用:
du -sch /var/lib/docker/containers/*/*-json.log | tail -1如果这个数字在持续增长且没有稳定下来,说明某个容器的 max-size 没生效——最常见的原因是那个容器是在配置修改之前创建的。
第二个是日志增长速率。比较两天同一时刻的文件大小,差值除以时间就是速率。一个正常的应用容器,每天的日志量应该在同一量级波动;如果突然涨了十倍,通常是应用在刷错误日志,日志问题只是表象。
第三个是轮转是否真的在发生。看目录里是否存在 -json.log.1、.2 这些文件:
ls -la /var/lib/docker/containers/*/ | grep -c 'json.log.[0-9]'一个跑了几天、日志量中等的容器,应该有至少一个轮转出来的备份文件。如果全是 -json.log 没有编号文件,要么日志量太小还没触发,要么 max-size 设得太大。
最后补一个常被忽略的细节:/var/lib/docker 所在的文件系统如果和根分区是同一个,日志涨起来会把根分区一起撑满,系统日志、包管理、临时文件全部受影响。有条件的站点应该把 /var/lib/docker 单独挂一块盘,把这类「不受控增长」的容量风险隔离出去。这比任何日志配置都更根本。