Docker 磁盘占用突然变大,别只看 docker system df

199 次浏览9 条回复

适用于 Linux 上的 Docker Engine 24+。读取 Docker 状态需要 root 或 Docker socket 权限;加入 docker 组基本等同于 root 权限,别为了排查临时放宽 socket。先做只读检查:

docker version
docker info --format 'root={{.DockerRootDir}} driver={{.Driver}}'
docker system df -v
docker ps -a --size --no-trunc
docker builder du

docker system df -v 能拆出镜像、容器、卷和构建缓存,但容器的 json-file 日志、bind mount 指向的宿主机目录不一定能从这里看清。日志路径可以逐个核对:

docker ps -aq | while read -r id; do
  path=$(docker inspect -f '{{.LogPath}}' "$id")
  [ -n "$path" ] && du -h -- "$path"
done | sort -h | tail -n 20

du 若报权限不足,用已有的受控提权方式读取,不要改日志文件权限。再对可疑容器看挂载来源:

docker inspect -f '{{range .Mounts}}{{println .Type .Source "->" .Destination}}{{end}}' CONTAINER

bind mount 的空间要到实际 Source 所在文件系统查,Docker 数据根目录没变大也可能把宿主机别处分区写满。RECLAIMABLE 只表示 Docker 认为对象当前未被引用,不代表业务上可以删;先别直接跑 docker system prune -a --volumes。确认对象归属和保留要求后再定向清理,最后复查 docker system df -v、相关文件系统的 df -hT,并验证容器能正常重建和启动。

再补一个容易漏的分支:LogPath 为空不一定是没日志,也可能容器用了 daemon 默认值或 journald 等日志驱动。可以先确认:

docker info --format 'default={{.LoggingDriver}}'
docker inspect -f 'configured={{.HostConfig.LogConfig.Type}} path={{.LogPath}}' CONTAINER

configured 为空时看 daemon 默认值;如果实际是 journald,磁盘占用要结合 journalctl --disk-usage 和 journald 的保留配置查,不能只盯 Docker 数据根目录。

还有一种 dfdu 对不上的情况:进程仍握着已经删除的日志或临时文件。可以先把 Docker 根目录映射到实际文件系统,再做只读核对:

root=$(docker info --format '{{.DockerRootDir}}')
df -hT "$root"
du -xsh "$root"
findmnt -T "$root"
lsof +L1 2>/dev/null | grep -F -- "$root"

lsof 需要安装且通常需要 root 才能看全;命中时先确认 PID、文件归属和是否确实属于故障进程,再按该进程的正常轮转或重启流程处理。这个分支能解释“文件已经删了,空间却没回来”,不要直接删 Docker 数据目录。

如果块空间看着还有余量,但容器创建文件时报 No space left on device,再看一下 inode。overlay2 和容器内大量小文件可能先把 inode 用完:

root=$(docker info --format '{{.DockerRootDir}}')
df -hT "$root"
df -ih "$root"
du -x --inodes --max-depth=2 "$root" 2>/dev/null | sort -n | tail -n 20

最后一条依赖 GNU du,目录很大时会比较慢,先在低峰执行。它只用来定位目录,别直接改或删 overlay2containers 下面的文件;确认对象归属后仍然通过 Docker 的生命周期命令处理。

如果同一台运维机配了多个 Docker context,先确认 CLI 当前连的是哪个 daemon:

docker context show
docker context inspect "$(docker context show)" --format '{{json .Endpoints.docker.Host}}'
docker info --format 'root={{.DockerRootDir}} security={{json .SecurityOptions}}'

docker system df 只统计当前连接的 daemon。rootless Docker、rootful Docker 和远端 context 的数据目录彼此独立;尤其连到 ssh:// 或 TCP 端点时,dfdufindmnt 要在 daemon 所在主机执行。rootless 常见目录虽是 $HOME/.local/share/docker,但配置可以覆盖,还是以当前 daemon 返回的 DockerRootDir 为准。

构建机上还要留意多个 Buildx builder:docker builder du 不保证覆盖 docker buildx ls 里的其他 builder。装了 Buildx 插件的话,可以先只读核对:

docker buildx version
docker buildx ls
docker buildx du --builder BUILDER_NAME

逐个替换 BUILDER_NAMEdocker-container 驱动的缓存通常跟着对应的 BuildKit 容器和卷,远端 builder 的空间则在远端主机;所以本机 docker system df 偏小,不代表这些缓存不存在。先确认 builder 是否仍被 CI 使用,别直接删 buildx_buildkit_* 卷。

排完以后最好顺手确认日志上限,而且要注意 daemon 的新默认值不会追溯到已有容器。Docker Engine 24+ 可以先看单个容器实际拿到的配置:

docker inspect -f '{{json .HostConfig.LogConfig}}' CONTAINER

Compose 里用 json-file 时可以给服务显式设置:

logging:
  driver: json-file
  options:
    max-size: "10m"
    max-file: "3"

改完需按原部署流程重建容器才生效,单纯重启现有容器不够。重建前先确认业务能否中断,并保留原配置;重建后再跑一次 docker inspect,同时观察日志文件是否按预期轮转。

命名卷也别只按 docker system df -v 里的数字判断,先确认卷驱动和实际使用者:

docker volume inspect -f 'driver={{.Driver}} mount={{.Mountpoint}} opts={{json .Options}}' VOLUME
docker ps -a --filter volume=VOLUME --format '{{.ID}}\t{{.Names}}\t{{.Status}}'

只有 driver=localMountpoint 非空时,才适合在 daemon 所在主机继续跑 findmnt -T MOUNTPOINTdf -hT MOUNTPOINTdu -xsh -- MOUNTPOINT。如果是 NFS、块存储之类的卷插件,本机看到的目录大小不一定等于后端配额或快照占用,要去对应存储端核对。清理前也要把停止状态的容器算进去,不能因为当前没有运行中的挂载就判定卷没用了。

还有一个和 inode 不同的分支:宿主文件系统有余量,但单个容器的可写层碰到了配额。先看容器是否显式设置过存储上限,再确认 Docker 数据目录所在文件系统:

root=$(docker info --format '{{.DockerRootDir}}')
docker info --format 'driver={{.Driver}} root={{.DockerRootDir}}'
docker inspect -f 'storageOpt={{json .HostConfig.StorageOpt}}' CONTAINER
findmnt -T "$root" -o TARGET,SOURCE,FSTYPE,OPTIONS

StorageOpt 里如果有 size,容器可能在宿主机 df 仍有空间时先报空间不足。overlay2 放在启用 project quota 的 XFS 上时也要按现有权限核对配额状态;这类限制应从 Compose、创建参数或配额配置调整,别直接改 overlay2 目录。调整后重建容器,并在容器内外分别复查可写空间和实际写入。

还可能是容器可写层本身在涨。应用如果原地修改镜像层里已有的大文件,overlay2 会触发 copy-up,日志和卷都不大时也会突然占空间。可以先看:

docker inspect --size -f 'rw={{.SizeRw}} rootfs={{.SizeRootFs}}' CONTAINER
docker diff CONTAINER | head -n 100

docker diff 只列 A/C/D 路径,不表示字节数,但能帮忙缩小范围。若 SizeRw 很大,变化又集中在数据库、上传或缓存目录,后续应通过原部署配置把这类数据放到合适的卷或 bind mount;迁移前先停写并做一致性校验,不要直接处理 overlay2 目录。