容器日志把 40G 系统盘吃满:Docker 日志轮转的四个配置点(2026)

Docker 默认的 json-file 日志驱动没有大小上限,容器往 stdout/stderr 写多少就存多少,路径是 /var/lib/docker/containers/<容器ID>/<容器ID>-json.log。 治理只要一个配置:在 /etc/docker/daemon.json 里给 log-opts 设 max-size 和 max-file,然后重启 Docker。但真正容易出事的不是「怎么配」,而是三件事:daemon.json 只对新创建的容器生效、JSON 写错会让 Docker 起不来、日志文件被删了但空间不释放。2026 年 9 月我碰到的现场是一台 40G 系统盘的小机器,一个调试中的服务把日志写到 27G,df -h 显示 100%,MySQL 直接拒绝写入。

上面是结论。上一篇写 Docker Compose 编排时,我特意跳过了日志配置——那是个独立的大坑,值得单独一篇。这篇把它补全。

Docker 日志写入与轮转路径

日志治理五步

为什么容器日志能无声无息吃满盘?

结论:因为默认配置里根本没有上限,而容器的 stdout 是把应用日志重定向到一个普通文件,没有任何系统级的保护机制。

很多人以为系统里有 logrotate 就会自动管住,其实不会——logrotate 管的是 /var/log 下那些按约定命名的文件,Docker 的容器日志在 /var/lib/docker/containers/ 里,完全在它的视野之外。

先确认当前状态:

$ docker info --format '{{.LoggingDriver}}'
json-file
$ docker system df -v

docker info 告诉你默认驱动是什么(绝大多数发行版装完都是 json-file),docker system df -v 会列出每个镜像、容器、卷、构建缓存占的空间。注意它统计的是容器可写层,不含日志文件——所以经常出现「docker system df 显示只用了 8G,df -h 却说盘满了」的情况。

真正的定位命令:

$ sudo du -sh /var/lib/docker/containers/*/*-json.log | sort -h | tail -5

这条按大小倒序列出所有容器日志。我当时看到的第一行是 27G /var/lib/docker/containers/a1b2.../a1b2...-json.log,对应的容器是一个还在调 API 的服务,它每秒钟往 stdout 打三行调试信息。

先确认 Docker 的数据目录到底在哪块盘。 如果 /var/lib/docker 被单独挂载到了数据盘,那你在它上面清出来的空间对系统盘告警毫无帮助:

$ docker info | grep -i "Docker Root Dir"
Docker Root Dir: /var/lib/docker
$ df -h /var/lib/docker
Filesystem      Size  Used Avail Use% Mounted on
/dev/vda1        40G   38G  1.2G  97% /

两条一起看,才知道「满的是哪块盘」。我见过把 Docker 目录迁到数据盘之后还在系统盘上找日志的,白折腾一小时。

步骤 1:定位大日志

全局默认怎么配?

结论:在 /etc/docker/daemon.json 里加 log-driver 和 log-opts 两段,改完 systemctl reload docker 或重启生效,但只影响之后新建的容器。

$ sudo tee /etc/docker/daemon.json > /dev/null <<'EOF'
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "50m",
    "max-file": "3"
  }
}
EOF

含义是:单个日志文件最大 50MB,最多保留 3 个(当前 + 2 个轮转),单容器日志上限 150MB。

改之前先做两件事,我这次就是因为漏了第一步多花了二十分钟:

$ sudo cp /etc/docker/daemon.json /etc/docker/daemon.json.bak
$ sudo dockerd --validate --config-file=/etc/docker/daemon.json

--validate 只检查配置合法性,不启动服务。如果 JSON 里多了一个逗号或者引号写错,直接 systemctl restart docker 会让 Docker 起不来,而所有容器一起挂——这台机器上还跑着数据库和面板,代价不小。

验证生效:

$ sudo systemctl restart docker
$ docker info --format '{{.LoggingDriver}}'
$ docker inspect --format '{{.HostConfig.LogConfig}}' 新容器名

官方配置说明在 Docker Docs · Configure logging drivers,max-size 支持 k/m/g 后缀,max-file 是轮转文件数。

步骤 2:配置 daemon.json

为什么老容器不生效?

结论:日志驱动和 log-opts 是创建容器时写进容器元数据的,改 daemon.json 不会回溯修改已有容器——必须重建。

这是最容易误判的一点。我配完重启 Docker,docker info 显示正常,但那个 27G 的容器继续长。查一下就明白了:

$ docker inspect --format '{{.HostConfig.LogConfig}}' 容器名
{json-file map[]}

map[] 说明这个容器的 log-opts 是空的,它还在用创建时的旧配置。daemon.json 只是「新建容器时的默认值」。

处理方式有两种:

方式一:重建容器。 对 compose 管理的服务很简单:

$ docker compose down
$ docker compose up -d

方式二:给单个服务在 compose 里直接写死。 这其实更推荐,因为配置跟着代码走,换机器不会漏:

services:
  api:
    image: myapp:1.4.2
    logging:
      driver: json-file
      options:
        max-size: "50m"
        max-file: "3"

compose 里的 logging 优先级高于 daemon.json,两处写不同值时以 compose 为准。我现在所有服务的 compose 文件里都带这一段,daemon.json 只作为兜底,防止有人手动 docker run 起测试容器时忘了加参数。

步骤 3:重建容器生效

已经写满了,怎么立刻救回来?

结论:不要 rm 日志文件——被删除但被进程持有的文件不会释放空间,正确做法是 truncate -s 0。

这个坑非常经典。rm 之后 df -h 依然 100%,因为 Docker 进程还持有那个文件句柄,inode 没释放。用 lsof 能看到:

$ sudo lsof +L1 | grep json.log

带 (deleted) 标记的就是「已删未释放」。正确清理方式:

$ sudo truncate -s 0 $(docker inspect --format='{{.LogPath}}' 容器名)

truncate 把文件长度改成 0 但不破坏 inode,句柄保持有效,空间立刻回收。这条命令可以直接在故障现场用,不需要停容器。

如果一次要清一批:

$ for c in $(docker ps -q); do
    p=$(docker inspect --format='{{.LogPath}}' "$c")
    [ -f "$p" ] && sudo truncate -s 0 "$p"
  done
$ df -h /

注意:truncate 是应急,不是方案。 不清根因,日志还是会继续涨,只是给了你时间去重建容器。

步骤 4:应急清空日志

journald 驱动要不要用?

结论:如果机器上所有服务都是容器、且你习惯用 journalctl 排错,可以切 journald 并设上限;否则保持 json-file + max-size 更省心。

切 journald:

{
  "log-driver": "journald",
  "log-opts": {
    "max-size": "50m"
  }
}

但 journald 自己也有容量限制,得单独设:

$ sudo mkdir -p /etc/systemd/journald.conf.d
$ sudo tee /etc/systemd/journald.conf.d/size.conf > /dev/null <<'EOF'
[Journal]
SystemMaxUse=1G
SystemMaxFileSize=100M
EOF
$ sudo systemctl restart systemd-journald

这几个参数的含义在 systemd journald.conf 手册 里有完整说明,包括 SystemMaxUse(全部日志的总上限)和 SystemMaxFileSize(单个文件的上限)的区别——前者管总量,后者管单片,两个都要设。

不设的话,journald 默认按磁盘容量的 10% 上限走,40G 的盘就是 4G,比 150MB 宽松得多,等于没治理。

我最后没切。原因是 docker logs -f 在 journald 驱动下行为会变(日志被 journald 接管,docker logs 仍需读 journal),排错习惯要改;而 json-file + 150MB 上限对这台机器已经够了。

步骤 5:journald 上限

顺带把其他几处占空间的也管住

结论:日志只是 Docker 占盘的一路,镜像、构建缓存、悬空卷三处加起来往往比日志更多。

$ docker system df
$ docker image prune -a --filter "until=168h"    # 清 7 天前未使用的镜像
$ docker builder prune --filter "until=168h"     # 清构建缓存
$ docker volume ls -f dangling=true              # 先看悬空卷,别直接删

docker image prune -a 会删掉所有没有被容器引用的镜像,包括你觉得「以后还会用」的——先看清楚再执行。我一般只加 --filter until=168h,让最近一周用过的留着。

悬空卷要特别小心:有些容器的数据卷在 docker compose down 之后变成 dangling,里面可能是数据库文件。先 docker volume inspect 看一眼挂载点和创建时间再决定。

一条 cron 监控,比事后救火便宜

结论:与其等盘满,不如在 80% 时收到告警。

$ cat > /usr/local/bin/disk-alert.sh <<'EOF'
#!/usr/bin/env bash
THRESHOLD=80
USED=$(df -P / | awk 'NR==2 {gsub("%","",$5); print $5}')
if [ "$USED" -ge "$THRESHOLD" ]; then
  curl -s -X POST "https://open.feishu.cn/open-apis/bot/v2/hook/你的token" \
    -H "Content-Type: application/json" \
    -d "{\"msg_type\":\"text\",\"content\":{\"text\":\"磁盘使用率 ${USED}%,请检查\"}}"
fi
EOF
$ sudo chmod +x /usr/local/bin/disk-alert.sh
$ (crontab -l 2>/dev/null; echo "*/30 * * * * /usr/local/bin/disk-alert.sh") | crontab -

每 30 分钟检查一次,超过 80% 推飞书。这套东西加起来不到 20 行,但能让你在「MySQL 拒绝写入」之前半小时知道出事了。

常见问题

Q:max-size: 50m 和 50M 有区别吗?
A:Docker 的解析不区分大小写,50m、50M 都认。但别写成 50mb,那会解析失败导致 Docker 启动报错。

Q:设了 max-size,为什么日志还是很大?
A:先 docker inspect 看 .HostConfig.LogConfig。如果是 map[],说明这个容器创建时还没这条配置,得重建。

Q:docker logs 显示不全,是日志丢了吗?
A:轮转之后老的被删了,docker logs 只能看到当前文件加轮转文件里的内容。要长期留存得配 log-driver 为 syslog/fluentd 或挂采集器。

Q:能不能只给一个容器设,不改全局?
A:可以,docker run --log-opt max-size=50m --log-opt max-file=3,或者 compose 里写 logging 段。推荐后者,配置可版本化。

Q:清空日志会影响正在运行的服务吗?
A:truncate -s 0 不会。文件句柄还在,进程继续往后写,只是内容清空了。rm 才会出问题。

相关阅读

上一篇 留言板一夜被刷两千条:用 Turnstile + Worker 做无感人机验证(2026)