Featured image of post 20G 的硬盘被 Docker 吃干净了,我把清理这件事理顺了

20G 的硬盘被 Docker 吃干净了,我把清理这件事理顺了

从磁盘告警到定位到 Docker 占了多少,再把镜像、容器、卷和构建缓存一层层清掉,顺带说说哪些不能乱删。

那天早上是被监控机器人的 Telegram 消息震醒的。

[WARN] rootfs usage 92%,我那台 20G 硬盘的小机器。前一天晚上看还是 78%,一夜之间涨了十几个点。

登上去 df -h 一看,/ 确实快满了。但奇怪的是,du -sh /var/lib/docker 只有 9 个多 G,和我印象里的数字差不多。真正占地方的东西藏在哪儿了。

这次排查让我把 Docker 的磁盘占用彻底搞清楚了一遍,记下来。

先搞清楚它到底占了多少

1
docker system df -v

这个命令比 docker system df 详细得多,它会把镜像、容器、本地卷、构建缓存四类分开列,每一类还列出具体的条目和大小。

我第一次跑出来的结果是这样的:

1
2
3
4
5
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          42        9         18.6GB    13.2GB (71%)
Containers      11        8         210MB     180MB (85%)
Local Volumes   17        6         3.1GB     1.4GB (45%)
Build Cache     88        0         2.4GB     2.4GB

十八个 G 的镜像,只有九个是活跃的。这就是问题所在。

我这两年在同一台机器上试过太多东西,什么 GitLab、Drone、MinIO、各种数据库版本,装完觉得不好用就 docker compose down 了,容器没了,镜像还躺在那儿。每个都几百兆,攒起来就是十几个 G。

镜像怎么清

最直接的是清掉那些没有标签的悬空镜像:

1
docker image prune

它只会删掉 <none>:<none> 那种,也就是构建过程中产生的中间层,或者被新版本顶掉标签的旧镜像。这个操作是安全的,因为没有任何容器在用它们。

如果想连"有标签但没有容器使用"的一起删:

1
docker image prune -a

这个就要小心了。它会问你确认,确认之后把所有没被容器引用的镜像全删掉。我当时脑子一热跑了这个,结果把我准备好的 mariadb:10.11 也删了。虽然能重新拉,但那台机器的网络拉大镜像很慢,花了十几分钟。

后来我的做法是先看清单再决定:

1
docker image ls --filter "dangling=false" --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}\t{{.CreatedSince}}"

按创建时间排序,心里有个数,再挑着删。

单个删:

1
docker image rm <IMAGE_ID>

如果提示被容器引用,先看是哪个容器:

1
docker ps -a --filter ancestor=<IMAGE_ID>

容器和它的可写层

docker ps -a 里那些 Exited 状态的容器,很多人以为删了容器就干净了,其实容器本身的可写层也占空间。

1
docker container prune

这个只删已停止的容器。有几点要提醒。

容器删了,它的可写层就没了。如果你在容器里手动改过什么文件、装过什么包,没写进镜像也没挂载出来,那部分就彻底丢了。我之前有个容器,为了图省事直接在里面 apt install 了个工具,后来 prune 一跑,第二天发现功能没了,又得重来一遍。

所以正确做法是,要么写进 Dockerfile,要么挂载出来,别指望容器的可写层能长期保存东西。

数据卷是最需要小心的一类

1
docker volume prune

这个命令删的是"没有被任何容器引用"的卷。听起来很安全,实际上有个陷阱。

我那次跑完之后,发现一个早就停掉的测试容器的数据卷被删了。虽然那个容器本来也没用了,但里面存着我早期的一些实验数据。删掉之后想找回来是不可能了。

所以我的建议是,跑之前先列出来看看:

1
docker volume ls --filter "dangling=true"

确认这些名字你都不认识、也不想要,再执行 prune。

更稳的做法是一个一个删:

1
docker volume rm <VOLUME_NAME>

麻烦,但至少你知道自己删了什么。

还有一种情况要注意。如果卷的名字是 Compose 自动生成的(形如 项目名_卷名),你 docker compose down 的时候不加 -v,卷是会保留的。加了 -v 就会连卷一起删。这个 -v 我在文档里看到过好几次,但真正记住是在一次误操作之后。

构建缓存:最容易被忽略的大头

这个是我这次排查才发现的大户。

1
docker builder prune

它会删掉 BuildKit 的缓存。现在的 Docker 默认用 BuildKit,每次 docker build 都会把中间层缓存下来,下次构建能复用。这个机制很好,但缓存不会自己过期,越攒越多。

我那次看到 2.4G,基本上都是几个月前折腾某个项目留下的。

想连所有缓存一起清:

1
docker builder prune -a

-a 是 all,包括那些还在被引用的缓存。清完之后下次构建会慢一些,但空间能立刻回来。

还有个带时间过滤的用法,我觉得挺实用:

1
docker builder prune --filter "until=168h"

只删七天以前的。这样常用的缓存还能留着,老的自然淘汰。

一键清理,以及为什么我不推荐

网上到处都写着这条:

1
docker system prune -a --volumes

它会把停止的容器、没用的网络、所有没被引用的镜像、所有没被引用的卷、所有构建缓存,一次性全清掉。

确实爽,我那次跑完从 92% 降到了 51%。

但我不推荐把它当成日常操作。原因有三个。

它不问你就删卷。数据卷里可能有你还没意识到重要的东西。

它把镜像全清了。下次起服务要重新拉,如果网络不好,等于给自己挖坑。

它不区分环境。你在开发机上跑和在跑着线上服务的机器上跑,风险完全不一样。我有一次在生产机上顺手跑了,把一个还没启动但准备切换的旧版本镜像删了,回滚的时候抓瞎。

我现在的固定动作

经过这次,我给自己定了一套流程,每个月做一次。

先看占用:

1
docker system df -v

然后按风险从低到高依次清:

1
2
3
docker container prune -f
docker image prune -f
docker builder prune -f --filter "until=336h"

这三条都是只删"明确没用"的东西,相对安全。卷我单独处理,先 docker volume ls --filter dangling=true 看清单,确认了再手动删。

再加上一条,给日志设上限。这个比清理更治本。Docker 默认的 json-file 驱动不限制大小,一个话多的服务能写到几个 G。我在每个 compose 文件里都加了:

1
2
3
4
5
logging:
  driver: json-file
  options:
    max-size: "10m"
    max-file: "3"

这样单个容器最多占 30M 日志,怎么跑都撑不爆。

顺手发现的一个问题

清理的时候我注意到 /var/log 下面也有几个 G。是 journald 的日志,默认能占到磁盘的 10%。

1
journalctl --disk-usage

看到是 1.8G。我改了 /etc/systemd/journald.conf:

1
SystemMaxUse=200M

然后 systemctl restart systemd-journald,空间立刻回来了。

这个问题和 Docker 没关系,但它们是同一类问题,都是"某个东西默认不限制自己,跑久了就把盘吃满"。清理只是补救,设上限才是根治。

现在的状态

清完之后稳定在 55% 左右,跑了两个月没再告警。

我也顺手在监控里加了一条,磁盘超过 80% 就推消息。这次是被 92% 才叫醒的,有点晚。早点知道,处理起来从容得多。

回头看,这件事本身没什么技术含量,真正花时间的是搞清楚哪些能删、哪些不能。Docker 把很多东西藏起来了,system df -v 这个命令就是那把钥匙。知道东西在哪儿,才谈得上管理。

使用 Hugo 构建
主题 Stack 由 Jimmy 设计
寻 Mi 处载入中...