Docker 容器日志撑满根分区:排查、清理与日志轮转配置|AlmaLinux 实战
背景
一台 AlmaLinux 服务器根分区告警。df -h 显示 /dev/vda3 共 49G、已用 49G、仅剩 43M,Use% 100%。同一份输出里还出现了十几条 overlay 挂载,挂载点是 /var/lib/docker/rootfs/overlayfs/<容器ID>,全部显示 100%。
这类 overlay 不是独立块设备,底层仍然落在根分区上。它们显示 100% 只是「跟随」了根分区的状态,并不代表各自占了一份独立空间。真正要查的还是根分区里哪个目录把空间吃掉了。
第一步:确认是空间满还是 inode 满
先看两件事:
df -h
df -idf -h 看块空间,df -i 看 inode。inode 用尽时应用同样会报「No space left on device」,但 df -h 的空间可能还有富余。本机是空间满,/dev/vda3 100%。
第二步:逐层定位大目录
从根分区往下逐层下钻,-x 保证不跨文件系统:
sudo du -xh --max-depth=1 / 2>/dev/null | sort -h找到最大的目录后继续往里查,例如 /var:
sudo du -xh --max-depth=1 /var 2>/dev/null | sort -h
sudo du -xh --max-depth=1 /var/lib 2>/dev/null | sort -h也可以装 ncdu 交互式查看,比 du 直观:
sudo dnf install -y ncdu
sudo ncdu -x /这一步很容易走偏:一开始怀疑的是业务目录 /opt/compose,但 df -h /opt/compose 只是复述了根分区的状态(它并不是独立挂载点),真正的大头在别处。判断某个目录是不是独立分区,看 df 的 Mounted on 列即可。
第三步:定位到 Docker
对 /var/lib/docker 逐层统计:
sudo du -h --max-depth=1 /var/lib/docker 2>/dev/null | sort -h输出:
0 /var/lib/docker/plugins
0 /var/lib/docker/runtimes
0 /var/lib/docker/swarm
0 /var/lib/docker/tmp
24K /var/lib/docker/volumes
56K /var/lib/docker/image
204K /var/lib/docker/network
3.7M /var/lib/docker/buildkit
3.2G /var/lib/docker/rootfs
30G /var/lib/docker/containers
33G /var/lib/dockercontainers 一个目录就占了 30G,占整个 Docker 数据目录的九成。这个目录里体积最大的通常是每个容器的标准输出日志文件 *-json.log。
第四步:找出是哪个容器在写
直接按大小列出所有容器的日志文件:
du -sh /var/lib/docker/containers/*/*-json.log 2>/dev/null | sort -h | tail -20想同时看到容器名:
for f in /var/lib/docker/containers/*/*-json.log; do
id=$(basename $(dirname "$f"))
name=$(docker inspect --format '{{.Name}}' "$id" 2>/dev/null)
size=$(du -h "$f" | cut -f1)
echo "$size $name ($id)"
done | sort -h到此就能确定是哪个(或哪几个)容器的日志失控。
为什么会涨到 30G
Docker 默认使用 json-file 日志驱动,写标准输出和标准错误的每一条内容都会落到 /var/lib/docker/containers/<ID>/<ID>-json.log。默认不做任何轮转——没有 max-size、没有 max-file,文件就只会一直追加。一个持续输出日志的服务,几周就能把根分区写满。
第五步:查看当前日志限制
确认 Docker 当前配置:
docker info | grep -iE 'log|storage'典型输出:
Storage Driver: overlayfs
Logging Driver: json-file
Log: awslogs fluentd gcplogs gelf journald json-file local splunk syslog这里 Logging Driver: json-file 是当前默认驱动;最后那行是 Docker 支持的全部驱动清单,不是当前配置。docker info 不会直接显示 max-size。
要精确看每个容器的日志配置:
docker inspect --format '{{.Name}} -> {{.HostConfig.LogConfig}}' $(docker ps -aq)如果输出是 map[],就是没有任何限制:
/<container-a> -> {json-file map[]}
/<container-b> -> {json-file map[]}如果已经有 max-file / max-size,则形如:
/<container-a> -> {json-file map[max-file:3 max-size:10m]}
/<container-b> -> {json-file map[max-file:10 max-size:5m]}Docker 的日志限制有三个可能的来源,优先级从低到高:
daemon.json里的全局log-opts;- 命令行启动参数
dockerd --log-opt ...(可被daemon.json覆盖); docker-compose.yml里单个 service 的logging.options——这一层会覆盖前两层。
所以如果容器已经有 max-size 但 daemon.json 不存在,很可能是 compose 文件里配的。
第六步:清理历史日志
先清掉已经写满的日志文件,立刻释放空间。truncate -s 0 只清空内容、不删文件,Docker 仍在正常写同一个 fd,不会影响运行中的容器:
for f in /var/lib/docker/containers/*/*-json.log; do
truncate -s 0 "$f"
done
# 确认
df -h /
du -sh /var/lib/docker/containers不要用 rm 删日志文件——文件被删除后 Docker 进程仍持有 fd,空间不会释放,df 看着还是满,只有重启容器才能回收。
第七步:配置日志轮转(如果还没有)
先确认 /etc/docker/daemon.json 是否存在:
ls -l /etc/docker/daemon.json
cat /etc/docker/daemon.json 2>/dev/null不存在就新建,全局限制单容器日志最多 3 个文件、每个 100M:
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json > /dev/null <<'EOF'
{
"log-driver": "json-file",
"log-opts": {
"max-size": "100m",
"max-file": "3"
}
}
EOF
# 让配置生效
sudo systemctl restart docker如果容器已经在 compose 里单独配了 logging,也可以把限制写到 compose 文件里,更直观:
services:
your-service:
logging:
driver: json-file
options:
max-size: "100m"
max-file: "3"关键点:daemon.json 只对新容器生效
daemon.json 里的 log-opts 不会修改已经存在的容器的配置。已有容器要继续用 truncate 清,或者重建才能吃到新限制。
用 compose 重建:
cd /path/to/compose/project
docker compose down
docker compose up -d顺序很重要:先改 daemon.json → 重启 Docker → 再 down/up,否则新容器读到的还是旧配置。
两个注意点:
- 不要加
-v。docker compose down默认保留 volumes,docker compose down -v会删数据卷,可能直接丢库。 - 如果 compose 文件里已经写了
logging,会覆盖daemon.json,以 compose 为准。
重建后再验证:
docker inspect --format '{{.Name}} -> {{.HostConfig.LogConfig}}' $(docker ps -aq)一个容易困惑的点:配置已生效,但空间没释放
实际排查中会出现这种情况——检查发现所有容器早就带上了 max-file:3 max-size:10m,说明限制已经生效(可能来自 compose 的 logging 段),但 df -h / 依然 100%,/var/lib/docker/containers 还是 30G。
原因是:轮转只约束新增日志。在限制生效之前已经写下的那个大文件,Docker 不会主动截断;只有当新日志达到 max-size、触发轮转时,旧文件才会被逐步挤掉。但一个已经 30G 的文件,可能永远等不到那一天。
此时唯一要做的就是 truncate -s 0,把那批历史文件清空,立刻回收空间。之后再让轮转机制接管,不会再涨回去。
可复用的排查清单
整条链路走完,可以固化成下面这组命令,下次直接按顺序跑:
# 1. 是空间满还是 inode 满
df -h
df -i
# 2. 逐层定位大目录
sudo du -xh --max-depth=1 / 2>/dev/null | sort -h
# 3. 如果是 Docker 宿主机,直接看 Docker 各子目录
sudo du -h --max-depth=1 /var/lib/docker 2>/dev/null | sort -h
# 4. 定位到具体容器日志
du -sh /var/lib/docker/containers/*/*-json.log 2>/dev/null | sort -h | tail
# 5. 看当前日志限制
docker inspect --format '{{.Name}} -> {{.HostConfig.LogConfig}}' $(docker ps -aq)
# 6. 清理(不要 rm)
for f in /var/lib/docker/containers/*/*-json.log; do truncate -s 0 "$f"; done
df -h /
# 7. 兜底:查有没有「已删除但仍被占用」的文件
sudo lsof +L1 | head最后一步针对的是另一类经典陷阱:文件已经被删,但仍有进程持有 fd,df 显示满而 du 找不到。这种要重启对应进程才能释放,truncate 解决不了。
结论
df里大量overlay条目跟随底层分区的状态,不是独立占用,别被条目数量误导。- Docker 默认
json-file驱动不轮转,容器日志是根分区被撑满的高频原因。 - 日志限制可以来自
daemon.json、dockerd启动参数或 compose 的logging,后者优先级最高。 daemon.json只对新容器生效,已有容器要么重建、要么继续用truncate清。- 日志轮转约束的是新增日志,历史巨型日志文件必须手动
truncate才能回收空间;绝不能用rm。 - 如果
df满、du找不到对应大文件,用lsof +L1查是否有进程持有已删除文件的 fd。
