【Docker】磁盘空间被占满怎么清理?------overlay2、容器日志与 Build Cache 排查实战
服务器根分区只剩几百 MB,容器开始报 no space left on device,重启 Docker 也没有用;进入 /var/lib/docker 后,overlay2、containers、volumes 每个目录都很大,却没人敢删。这个场景真正难的不是缺少清理命令,而是不知道空间属于哪个 Docker 对象,也不知道删除动作会不会带走数据库、上传文件或下次发布需要的镜像。本文从磁盘、Docker 对象和容器内部三个视角建立证据链,再按风险从低到高处理日志、可写层、镜像、构建缓存和数据卷。

1. 先判断:真的是 Docker 把磁盘写满了吗
看到 /var/lib/docker 很大,不等于立刻执行 docker system prune -a --volumes。第一步应该确认是容量耗尽、inode 耗尽,还是一个已经删除但仍被进程打开的文件占着空间。三种情况都可能表现为"无法创建文件",处理方式却完全不同。
bash
df -h
df -i
sudo lsof +L1
docker info --format '{{.DockerRootDir}}'
df -h 看文件系统的块空间;df -i 看 inode。大量几 KB 的小日志、缓存或临时文件可能先耗尽 inode,此时磁盘容量看起来还有余量。lsof +L1 用来找链接数已经为 0、但仍被进程打开的文件:你在目录里看不到它,du 统计也可能变小,df 却没有释放。让持有文件描述符的进程正常重开日志,空间才会真正归还。
然后确认 Docker Root Dir。默认位置常见为 /var/lib/docker,但生产环境可能迁到独立数据盘,Docker Desktop 还可能把数据放在虚拟磁盘中。不要把教程里的默认路径当作本机事实。Docker Engine 29.0 及之后的新安装默认使用 containerd image store,官方文档也提示它使用 snapshotter 取代经典存储驱动;旧环境中常见的 overlay2 仍大量存在,所以本文保留两套视角,但清理都通过 Docker 自己的对象命令完成。

上图把最常见的五类占用拆开:镜像层、容器可写层、日志、数据卷和构建缓存。overlay2 只是旧式存储布局的一部分,不是一个可以按目录名随意删除的缓存池。镜像层会被多个镜像和容器共享,容器可写层与具体容器关联;目录之间存在元数据引用,手删一个看似"很久没改"的哈希目录,可能让运行中容器或后续重启直接失败。
2. 用三组数字把空间映射到对象
排查时建议同时保存下面三组证据,而不是只贴一张 du 截图。
bash
docker_root="$(docker info --format '{{.DockerRootDir}}')"
df -h "$docker_root"
sudo du -xhd1 "$docker_root" | sort -h
docker system df -v
docker ps -as
df 回答整个文件系统还有多少空间;du 回答 Docker Root Dir 下哪个物理目录更大;docker system df -v 则把占用翻译成镜像、容器、Local Volumes 和 Build Cache。最后的 docker ps -as 会显示容器可写层大小。四条命令放在一起,才能判断"物理目录很大"和"Docker 认为可回收"是不是同一件事。

docker system df 输出中的 RECLAIMABLE 也不能机械理解成"现在删掉绝对安全"。它表达的是 Docker 对象引用关系,不理解业务语义。某个镜像没有容器引用,可能仍是回滚版本;某个匿名卷没有容器引用,里面也可能存在尚未确认的导入数据。技术上的未使用,与业务上的不需要,不是同一个判断。
建议把现场记录成表格:文件系统使用率、Docker Root Dir、存储后端、日志驱动、各类对象占用、最大容器可写层、最大日志文件、最大卷、最近构建任务。这样清理后可以精确说明释放了什么,而不是只留下"执行 prune 后好了"。
#mermaid-svg-rKZVtHWHMDop1Lc2{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-rKZVtHWHMDop1Lc2 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-rKZVtHWHMDop1Lc2 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-rKZVtHWHMDop1Lc2 .error-icon{fill:#552222;}#mermaid-svg-rKZVtHWHMDop1Lc2 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-rKZVtHWHMDop1Lc2 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-rKZVtHWHMDop1Lc2 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-rKZVtHWHMDop1Lc2 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-rKZVtHWHMDop1Lc2 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-rKZVtHWHMDop1Lc2 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-rKZVtHWHMDop1Lc2 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-rKZVtHWHMDop1Lc2 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-rKZVtHWHMDop1Lc2 .marker.cross{stroke:#333333;}#mermaid-svg-rKZVtHWHMDop1Lc2 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-rKZVtHWHMDop1Lc2 p{margin:0;}#mermaid-svg-rKZVtHWHMDop1Lc2 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-rKZVtHWHMDop1Lc2 .cluster-label text{fill:#333;}#mermaid-svg-rKZVtHWHMDop1Lc2 .cluster-label span{color:#333;}#mermaid-svg-rKZVtHWHMDop1Lc2 .cluster-label span p{background-color:transparent;}#mermaid-svg-rKZVtHWHMDop1Lc2 .label text,#mermaid-svg-rKZVtHWHMDop1Lc2 span{fill:#333;color:#333;}#mermaid-svg-rKZVtHWHMDop1Lc2 .node rect,#mermaid-svg-rKZVtHWHMDop1Lc2 .node circle,#mermaid-svg-rKZVtHWHMDop1Lc2 .node ellipse,#mermaid-svg-rKZVtHWHMDop1Lc2 .node polygon,#mermaid-svg-rKZVtHWHMDop1Lc2 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-rKZVtHWHMDop1Lc2 .rough-node .label text,#mermaid-svg-rKZVtHWHMDop1Lc2 .node .label text,#mermaid-svg-rKZVtHWHMDop1Lc2 .image-shape .label,#mermaid-svg-rKZVtHWHMDop1Lc2 .icon-shape .label{text-anchor:middle;}#mermaid-svg-rKZVtHWHMDop1Lc2 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-rKZVtHWHMDop1Lc2 .rough-node .label,#mermaid-svg-rKZVtHWHMDop1Lc2 .node .label,#mermaid-svg-rKZVtHWHMDop1Lc2 .image-shape .label,#mermaid-svg-rKZVtHWHMDop1Lc2 .icon-shape .label{text-align:center;}#mermaid-svg-rKZVtHWHMDop1Lc2 .node.clickable{cursor:pointer;}#mermaid-svg-rKZVtHWHMDop1Lc2 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-rKZVtHWHMDop1Lc2 .arrowheadPath{fill:#333333;}#mermaid-svg-rKZVtHWHMDop1Lc2 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-rKZVtHWHMDop1Lc2 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-rKZVtHWHMDop1Lc2 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rKZVtHWHMDop1Lc2 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-rKZVtHWHMDop1Lc2 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rKZVtHWHMDop1Lc2 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-rKZVtHWHMDop1Lc2 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-rKZVtHWHMDop1Lc2 .cluster text{fill:#333;}#mermaid-svg-rKZVtHWHMDop1Lc2 .cluster span{color:#333;}#mermaid-svg-rKZVtHWHMDop1Lc2 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-rKZVtHWHMDop1Lc2 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-rKZVtHWHMDop1Lc2 rect.text{fill:none;stroke-width:0;}#mermaid-svg-rKZVtHWHMDop1Lc2 .icon-shape,#mermaid-svg-rKZVtHWHMDop1Lc2 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rKZVtHWHMDop1Lc2 .icon-shape p,#mermaid-svg-rKZVtHWHMDop1Lc2 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-rKZVtHWHMDop1Lc2 .icon-shape .label rect,#mermaid-svg-rKZVtHWHMDop1Lc2 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rKZVtHWHMDop1Lc2 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-rKZVtHWHMDop1Lc2 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-rKZVtHWHMDop1Lc2 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 空间满
inode 满
容器日志
可写层
Build Cache
数据卷
磁盘告警或 no space left on device
df -h 与 df -i
空间满还是 inode 满
定位 Docker Root Dir
检查海量小文件与日志
docker system df -v
最大来源
确认 LogPath 并配置轮转
docker ps -s 后进入容器定位目录
按时间过滤 builder prune
核对业务数据和备份
再次检查 df 与服务健康
3. 第一大来源:容器日志持续增长
Docker 的 json-file 日志驱动会把容器标准输出和标准错误写入文件。官方文档给出的默认 max-size 是 -1,也就是不做大小限制;如果应用遇到异常重试、DEBUG 日志打开、请求体完整打印,单个容器日志可以持续增长。先找每个容器对应的日志路径和大小:
bash
docker ps -aq | while read -r id; do
name=$(docker inspect --format '{{.Name}}' "$id" | sed 's#^/##')
path=$(docker inspect --format '{{.LogPath}}' "$id")
if [ -n "$path" ] && [ -f "$path" ]; then
bytes=$(stat -c '%s' "$path")
printf '%12s %-28s %s
' "$bytes" "$name" "$path"
fi
done | sort -nr
这个脚本把字节数放在第一列,按数值倒序排列,能直接看到最大日志属于哪个容器。不要用 find /var/lib/docker/containers -name '*-json.log' -delete。Docker 官方明确警告,json-file 文件应由 Docker daemon 独占管理,外部工具直接操作可能干扰日志系统并产生不可预期行为。
更稳的长期配置是在 /etc/docker/daemon.json 中设置轮转上限:
json
{
"log-driver": "json-file",
"log-opts": {
"max-size": "50m",
"max-file": "3"
}
}
log-opts 的值要写成字符串。修改 daemon 配置并重启 Docker 后,它只对新创建的容器自动生效,旧容器不会神奇地继承新设置;需要通过 Compose 或发布流程重新创建容器。若采用 local 日志驱动,还要结合组织现有日志采集方式验证兼容性,不能只看它默认带轮转就直接切换。

事故处理中如果日志已经挤满根分区,优先降低应用日志量、暂停异常流量或重建问题容器。清空日志只能作为有审批、有记录的止血动作,而且要确认日志是否需要留作审计和事故分析。止血完成后一定回到根因:是无限重试、日志级别错误,还是业务把大对象持续打印到了 stdout。
4. 第二大来源:容器可写层与 overlay2
镜像层应该尽量只读,数据库、上传目录、模型文件和需要持久化的缓存应该落到 volume 或 bind mount。若应用把文件写进一个没有挂载的容器目录,数据会进入容器可写层;容器运行越久,docker ps -s 的 SIZE 越大。先从对象视角找到异常容器:
bash
docker ps -as --format 'table {{.ID}} {{.Names}} {{.Size}} {{.Status}}'
docker inspect 容器名 --format '{{json .Mounts}}'
docker exec 容器名 sh -c 'du -xhd1 / 2>/dev/null | sort -h'
容器内的 du 与宿主机 docker ps -s 可能不完全一致,因为联合文件系统、已删除仍打开的文件、稀疏文件和挂载点会影响统计。排查目标不是让两组数字一模一样,而是找到没有挂载却持续写入的目录。例如 /tmp、应用工作目录、浏览器缓存、包管理缓存、转码中间文件、core dump,都是常见来源。
OverlayFS 的核心是 lowerdir、upperdir、workdir 和 merged 视图。镜像层位于 lowerdir,容器第一次修改镜像中的文件时会触发 copy-up,把文件复制到 upperdir 后再修改。删除容器内的文件也不等于改写底层镜像,它是在上层记录不可见标记。正因为存在这些分层引用,不能拿 du 找到一个最大的 overlay2/<hash> 后直接 rm -rf。
修复可写层膨胀通常有三步:把持久数据迁到明确的 volume 或 bind mount;给临时目录设置生命周期和容量;用镜像重建容器而不是把运行中容器当成长期虚拟机维护。完成迁移后再删除旧容器,可写层才会由 Docker 正常回收。
5. 第三大来源:镜像和 Build Cache
开发机、CI 节点和频繁发布的服务器经常不是日志最大,而是构建缓存与旧镜像累积。先看 docker system df -v,再按用途选择窄命令:
bash
# 只删悬空镜像
docker image prune
# 删除所有未被任何容器引用的镜像;执行前检查回滚版本
docker image prune -a
# 清理传统 builder 的未使用缓存
docker builder prune --filter 'until=168h'
# Buildx:保留空间预算或目标剩余空间
docker buildx prune --min-free-space 20gb
Buildx 官方文档支持 --max-used-space、--min-free-space 和 --reserved-space。这比每天无差别清空缓存更适合 CI:缓存本来就是用空间换构建时间,全部删除会让下一次构建重新下载依赖、重跑每层命令,可能把磁盘事故转成发布变慢。更合理的是给构建缓存设预算,并根据镜像仓库可用性、网络成本和发布频率决定保留窗口。

docker system prune 默认会删除停止的容器、未使用网络、悬空镜像和未使用构建缓存;加 -a 后会扩大到所有未被容器引用的镜像;加 --volumes 还会处理匿名卷。命令越短,影响范围可能越大。生产操作更推荐拆成 container、image、builder、network、volume 几类分别核对,并使用 until 等过滤条件控制范围。
#mermaid-svg-0DsrlfJT3cCPU2rz{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0DsrlfJT3cCPU2rz .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0DsrlfJT3cCPU2rz .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0DsrlfJT3cCPU2rz .error-icon{fill:#552222;}#mermaid-svg-0DsrlfJT3cCPU2rz .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0DsrlfJT3cCPU2rz .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0DsrlfJT3cCPU2rz .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0DsrlfJT3cCPU2rz .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0DsrlfJT3cCPU2rz .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0DsrlfJT3cCPU2rz .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0DsrlfJT3cCPU2rz .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0DsrlfJT3cCPU2rz .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0DsrlfJT3cCPU2rz .marker.cross{stroke:#333333;}#mermaid-svg-0DsrlfJT3cCPU2rz svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0DsrlfJT3cCPU2rz p{margin:0;}#mermaid-svg-0DsrlfJT3cCPU2rz defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-0DsrlfJT3cCPU2rz g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-0DsrlfJT3cCPU2rz g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-0DsrlfJT3cCPU2rz g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-0DsrlfJT3cCPU2rz g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-0DsrlfJT3cCPU2rz g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-0DsrlfJT3cCPU2rz .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-0DsrlfJT3cCPU2rz .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-0DsrlfJT3cCPU2rz .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-0DsrlfJT3cCPU2rz .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-0DsrlfJT3cCPU2rz .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-0DsrlfJT3cCPU2rz .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-0DsrlfJT3cCPU2rz .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-0DsrlfJT3cCPU2rz .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0DsrlfJT3cCPU2rz .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-0DsrlfJT3cCPU2rz .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0DsrlfJT3cCPU2rz .edgeLabel .label text{fill:#333;}#mermaid-svg-0DsrlfJT3cCPU2rz .label div .edgeLabel{color:#333;}#mermaid-svg-0DsrlfJT3cCPU2rz .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-0DsrlfJT3cCPU2rz .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-0DsrlfJT3cCPU2rz .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-0DsrlfJT3cCPU2rz .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-0DsrlfJT3cCPU2rz .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-0DsrlfJT3cCPU2rz .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0DsrlfJT3cCPU2rz .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0DsrlfJT3cCPU2rz #statediagram-barbEnd{fill:#333333;}#mermaid-svg-0DsrlfJT3cCPU2rz .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0DsrlfJT3cCPU2rz .cluster-label,#mermaid-svg-0DsrlfJT3cCPU2rz .nodeLabel{color:#131300;}#mermaid-svg-0DsrlfJT3cCPU2rz .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-0DsrlfJT3cCPU2rz .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-0DsrlfJT3cCPU2rz .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-0DsrlfJT3cCPU2rz .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-0DsrlfJT3cCPU2rz .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-0DsrlfJT3cCPU2rz .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-0DsrlfJT3cCPU2rz .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-0DsrlfJT3cCPU2rz .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-0DsrlfJT3cCPU2rz .note-edge{stroke-dasharray:5;}#mermaid-svg-0DsrlfJT3cCPU2rz .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-0DsrlfJT3cCPU2rz .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-0DsrlfJT3cCPU2rz .statediagram-note text{fill:black;}#mermaid-svg-0DsrlfJT3cCPU2rz .statediagram-note .nodeLabel{color:black;}#mermaid-svg-0DsrlfJT3cCPU2rz .statediagram .edgeLabel{color:red;}#mermaid-svg-0DsrlfJT3cCPU2rz #dependencyStart,#mermaid-svg-0DsrlfJT3cCPU2rz #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-0DsrlfJT3cCPU2rz .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-0DsrlfJT3cCPU2rz :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} docker build
构建完成
docker run
docker stop
docker rm
无容器引用
docker image prune
docker builder prune
挂载持久化目录
容器删除后仍可能保留
构建缓存
镜像
容器可写层
停止容器
可回收镜像
数据卷
6. 数据卷为什么必须最后处理
数据卷与容器生命周期分离,这是它能保存数据库数据的原因,也是清理时最危险的地方。容器删除后,命名卷通常仍然存在;匿名卷也可能因为 Compose 项目改名、容器重建或临时任务而留下。先列出卷,再逐个检查挂载关系和业务内容:
bash
docker volume ls
docker volume inspect 卷名
docker ps -a --filter volume=卷名
# 只读方式查看卷中一级目录大小
docker run --rm -v 卷名:/data:ro alpine sh -c 'du -xhd1 /data 2>/dev/null | sort -h'
不要把 docker volume ls -qf dangling=true 的结果直接传给 docker volume rm。dangling 表示当前没有容器引用,不代表没有业务价值。数据库备份、离线导入文件、已下线但仍在保留期的环境,都可能处于"无引用但不能删"的状态。更可靠的做法是给卷加项目、环境、负责人、保留期标签,删除前核对备份恢复演练。
如果空间来自 bind mount,它甚至不会出现在 Local Volumes 统计中,需要从 docker inspect 的 Mounts 找到宿主机 Source,再对该路径运行 du。这也是为什么只看 docker system df 会漏掉部分数据:它擅长统计 Docker 管理的对象,不负责替你理解宿主机任意目录的业务含义。
7. 五个常见失败案例
失败一:直接删除 overlay2 目录
这是风险最高的"快速清理"。目录名与镜像、容器、快照元数据存在引用关系,手删会制造 Docker 不知道的残缺状态。正确入口是删除对应容器或镜像,让 daemon 完成引用和层的回收。Docker 已经无法启动时,也应先扩容、挂载临时空间或迁移 Docker Root Dir,取得操作余量后再修复,而不是在满盘压力下随机删除哈希目录。
失败二:du 很小就认定 Docker 没问题
已删除但仍打开的日志文件不会再出现在普通目录遍历里,却仍占用文件系统块。用 lsof +L1 找持有者,安全地让进程重开文件。另一个差异来源是挂载边界:du -x 不跨文件系统,volume 或 bind mount 可能位于其他挂载点,要结合 findmnt 与 Mounts 检查。
失败三:配置日志轮转后旧容器仍继续增长
daemon 默认日志配置通常只影响新创建容器。仅执行 docker restart 可能不会重建容器配置。应确认 docker inspect 中实际的 HostConfig.LogConfig,并通过 Compose 重建,再观察新的 LogPath 与轮转文件是否按预期出现。
失败四:全量 prune 后无法快速回滚
未被当前容器引用的旧镜像可能正是回滚版本。发布前应确认镜像仓库仍可访问、tag 与 digest 可定位、构建上下文可复现。清理 CI 节点时则要接受缓存失效后的构建成本,最好用时间或空间过滤,而不是每天 -a。
失败五:误删数据卷
卷名随机、Compose 项目改名、环境长期停止,都会让卷看起来无人使用。删除前至少确认卷标签、挂载容器、一级目录、业务负责人和备份。数据库卷还要验证恢复路径;"已有备份"只有在恢复演练成功后才是有效保护。
失败六:只清理,不治理写入源头
日志级别没有改、异常重试没有限速、临时文件没有过期、构建缓存没有预算,今天释放 30 GB,明天仍会再次占满。清理记录里必须包含增长速度:一小时增加多少、由哪个容器产生、对应什么请求或任务。能解释增长速率,才有办法设置告警阈值和保留策略。
8. 可直接运行的只读审计脚本
素材包的 code/docker-disk-audit.sh 只读取信息,不执行删除。它先定位 Docker Root Dir 和文件系统状态,再输出 Docker 对象、容器可写层、日志路径与卷列表。脚本已经用 bash -n 做语法检查;当前写作环境没有安装 Docker CLI,所以没有把伪造的 docker system df 输出冒充实测结果。
bash
chmod +x docker-disk-audit.sh
sudo ./docker-disk-audit.sh | tee docker-disk-audit.txt
运行后先看四处:df -h 是否超过告警线;df -i 是否 inode 紧张;docker system df -v 中哪一类对象最大且可回收;日志列表顶部是否集中在一两个容器。再结合 docker ps -s 判断是日志还是可写层。如果最大来源是卷,停止自动化清理,进入数据核对流程。
对于 Docker Desktop,命令判断对象仍然适用,但宿主机空间还可能被虚拟磁盘文件占用。删除镜像后,虚拟磁盘文件在 Windows 或 macOS 上未必立即缩小,因为逻辑空间释放与宿主文件压缩是两步。此时应使用 Docker Desktop 当前版本提供的磁盘管理或虚拟磁盘回收方式,不要照搬 Linux 的 /var/lib/docker 路径。
9. 生产环境的治理方案
日志治理建议同时做三层:应用侧限制重复异常与大字段;Docker 日志驱动设置大小和文件数量;采集端设置背压、保留期和远端存储。只做 daemon 轮转会丢失超出窗口的现场,只做远端采集又可能在网络中断时让本地日志继续增长。
镜像治理要保留可回滚版本。可以按环境设置最近 N 个发布 digest、最近若干天镜像和固定里程碑版本,其他未引用镜像才进入回收。构建节点使用 Buildx 空间预算,避免构建缓存无限增长;基础镜像与依赖缓存要结合网络质量决定保留量。
可写层治理的目标是让容器"可替换"。配置从环境或配置中心进入,持久数据进入 volume 或外部存储,临时数据进入受控目录,容器删除后不应丢失业务数据。这样 docker rm 才是常规维护动作,而不是一次数据赌博。
监控至少包含文件系统使用率、inode 使用率、剩余空间预计耗尽时间、Docker Root Dir 增长速度、最大容器日志、最大可写层、Build Cache 总量。告警不要只设 90% 一个阈值:容量很大的磁盘从 80% 到 100% 可能只需十分钟,增长速率比静态百分比更早暴露风险。
