Docker镜像层太多导致云盘空间耗尽怎么清理
Docker镜像层太多导致云盘空间耗尽怎么清理
Docker把云盘打满,现场一般不是某一个容器突然写爆了,而是镜像层、构建缓存、旧容器日志、未使用卷一起堆出来的。尤其是 CI/CD 机器、测试环境、频繁发版的业务机,跑一两个月不清理,/var/lib/docker 目录涨到几十 GB 很常见。
实际使用中发现,很多人看到 df -h 显示 / 分区 100%,第一反应是删业务日志,但删完发现空间没怎么回来。原因是 Docker 的空间占用不一定在业务目录里,默认大多在 /var/lib/docker,overlay2、containers、image、volumes 这几个目录都可能是大头。
先确认是不是 Docker 占满了盘
先看系统盘整体情况:
df -h
如果 / 或者 /var 所在分区已经 90% 以上,就要继续看 Docker 占用:
du -sh /var/lib/docker
再往下看哪个目录最大:
du -h --max-depth=1 /var/lib/docker | sort -hr
常见结果大概是这样:
/var/lib/docker/overlay2 很大:多数是镜像层和容器可写层堆积。
/var/lib/docker/containers 很大:多数是容器 stdout/stderr 日志没限制。
/var/lib/docker/volumes 很大:多数是数据卷里有数据库、缓存、上传文件,不能随便删。
/var/lib/docker/buildkit 很大:多数是 BuildKit 构建缓存。
这里补充一点,不建议直接 rm -rf /var/lib/docker/overlay2 下面的目录。overlay2 目录和 Docker 元数据有关联,手工删很容易把 Docker 状态搞乱,后面容器起不来,排查成本比扩盘还高。
用 docker system df 看 Docker 自己怎么统计
Docker 自带一个比较直观的命令:
docker system df
如果要看明细:
docker system df -v
输出里重点看 Images、Containers、Local Volumes、Build Cache。实际排障时,这个命令比直接 du 更适合判断“哪些可以通过 Docker 命令清掉”。
比如看到 RECLAIMABLE 很高,说明有不少空间 Docker 认为可以释放。注意,RECLAIMABLE 不是百分百安全删除的意思,尤其是 volumes,需要确认业务没有依赖。
清理已经停止的容器
先看停止状态的容器:
docker ps -a
很多测试环境会留下大量 Exited 容器,每个容器可能不大,但日志、可写层、临时文件累计起来也很可观。
清理所有已停止容器:
docker container prune
如果想先看数量:
docker ps -a -f status=exited
多说一句,docker container prune 不会删除正在运行的容器,线上执行风险相对可控,但还是建议先 docker ps -a 看一眼,避免把后续还想 inspect 的故障现场清掉。
清理悬空镜像 dangling image
频繁 docker build 后,容易出现大量
查看悬空镜像:
docker images -f dangling=true
清理悬空镜像:
docker image prune
这个命令只清理 dangling image,不会删掉仍然有 tag 的镜像。一般作为日常清理命令比较安全。
如果构建非常频繁,比如每天几十次镜像构建,dangling image 可能几天就堆出 10GB 到 50GB。云服务器系统盘只有 40GB 或 50GB 时,很容易直接把根分区顶满。
清理所有未使用镜像要谨慎
更激进的命令是:
docker image prune -a
它会删除所有“当前没有被容器使用”的镜像,不只是
这个命令在线上要谨慎。比如某个镜像现在没有容器在跑,但回滚时可能要用;执行后镜像没了,回滚就需要重新 pull。如果外网慢、镜像仓库限速、跨境链路抖动,就会把原本几秒的回滚拖成几分钟。
比较稳的做法是先列出镜像:
docker images
再结合容器使用情况看哪些镜像还在被引用:
docker ps --format "table {{.ID}}\t{{.Image}}\t{{.Status}}\t{{.Names}}"
确认旧版本镜像不需要保留后,再按镜像 ID 删除:
docker rmi IMAGE_ID
如果是业务发版机,可以保留最近 2 到 3 个版本镜像,太老的删掉。这个保留数量跟发版频率有关,日更业务和月更业务不能按一个标准处理。
BuildKit 构建缓存经常被忽略
现在很多环境默认开启 BuildKit,docker build 时会留下构建缓存。它能加速后续构建,但也会持续吃盘。
查看构建缓存:
docker builder du
清理构建缓存:
docker builder prune
如果要清理得更彻底:
docker builder prune -a
这里的 -a 同样要注意,它会把未使用的构建缓存都清掉。清完后下一次构建会慢一些,因为依赖层、编译层可能要重新跑。
实际使用中,CI 机器上 build cache 是高频大户。比如 Node.js、Java、Go 多语言项目混在一台构建机上,node_modules、Maven 依赖、编译中间层叠加,buildkit 目录涨到几十 GB 并不夸张。
容器日志可能比镜像层更狠
很多人以为 Docker 占盘就是镜像层,其实容器日志经常更夸张。默认 json-file 日志驱动如果不限制大小,应用疯狂输出日志时,一个容器日志文件就能写到几十 GB。
查看容器日志目录大小:
du -h --max-depth=1 /var/lib/docker/containers | sort -hr | head
查看具体日志文件:
find /var/lib/docker/containers -name "*-json.log" -size +1G -ls
不建议直接删除日志文件,正在被 Docker 占用的文件直接 rm 后,空间可能不会立刻释放,因为文件句柄还在。更稳的方式是清空文件内容:
truncate -s 0 /var/lib/docker/containers/容器ID/容器ID-json.log
如果要一次性清空所有容器 json 日志:
find /var/lib/docker/containers -name "*-json.log" -exec truncate -s 0 {} \;
这条命令对线上影响通常比较小,但会丢失容器标准输出日志。执行前确认日志已经采集到 ELK、Loki、云日志服务,或者业务不依赖本地 stdout 回溯。
提前限制 Docker 日志大小
清理只是止血,日志不限制还会继续涨。可以修改 /etc/docker/daemon.json:
{ "log-driver": "json-file", "log-opts": { "max-size": "100m", "max-file": "3" } }
含义是每个容器日志文件最大 100MB,保留 3 个文件。单容器最多约 300MB,至少不会无限增长。
修改后重启 Docker:
systemctl restart docker
注意,重启 Docker 可能影响容器,生产环境要看容器是否配置 restart policy,也要评估业务是否允许短暂中断。
如果容器是 docker run 启动的,也可以在启动时单独设置:
docker run --log-driver=json-file --log-opt max-size=100m --log-opt max-file=3 ...
卷 volumes 不要上来就 prune
Docker 卷很容易被误删。比如 MySQL、PostgreSQL、Redis、MinIO、GitLab 这类服务,数据可能都在 volume 里。
查看卷:
docker volume ls
查看卷占用:
du -h --max-depth=1 /var/lib/docker/volumes | sort -hr | head
清理未使用卷的命令是:
docker volume prune
但这条命令不建议在不确认业务结构的机器上随手执行。它会删除未被当前容器引用的 volume。有些停掉的服务虽然暂时没跑,但数据还在卷里,一 prune 就没了。
稳一点的方式是先 inspect:
docker volume inspect VOLUME_NAME
再结合 docker ps -a 看是否有容器使用这个卷。确认是废弃测试数据、临时环境残留,再删除:
docker volume rm VOLUME_NAME
docker system prune 适合救急,但要知道它删什么
很多现场为了快速释放空间,会执行:
docker system prune
它会清理停止容器、未使用网络、dangling image、构建缓存。默认不会删除 volume,也不会删除仍被容器使用的镜像。
更激进的是:
docker system prune -a
它会删除所有未被容器使用的镜像,释放空间明显更大,但也会带来镜像回滚和重新拉取的问题。
带 volume 的版本是:
docker system prune -a --volumes
这条在生产环境要非常克制。除非已经确认 volumes 里没有业务数据,否则不要为了释放空间直接上。云盘满了可以先扩容、迁移、清日志,别用删除数据换空间。
常见清理命令按风险拆开看
docker container prune:清理停止容器,风险较低,适合日常维护。
docker image prune:清理 dangling image,风险较低,适合构建环境。
docker builder prune:清理构建缓存,风险中等,影响后续构建速度。
docker image prune -a:清理未被容器使用的镜像,风险中等,可能影响回滚。
docker volume prune:清理未使用卷,风险偏高,可能误删数据。
docker system prune -a --volumes:清理范围很大,生产环境不建议当作常规命令。
云盘已经 100% 时的处理顺序
磁盘 100% 后,Docker 可能无法启动新容器,甚至 SSH 登录、写日志、systemd 操作都会异常。这个时候不要一上来重启服务,重启可能导致更多进程起不来。
可以先找几个低风险空间入口:
清空超大 Docker json 日志:find /var/lib/docker/containers -name "*-json.log" -size +500M -exec truncate -s 0 {} \;
清理停止容器:docker container prune
清理 dangling image:docker image prune
清理 BuildKit 缓存:docker builder prune
再看空间是否恢复:
df -h
如果 Docker 命令已经不能正常执行,优先清日志,因为 truncate 不依赖 Docker daemon。释放出几百 MB 到 1GB 后,再执行 Docker prune 类命令会稳很多。
镜像层为什么会越来越多
Docker 镜像是分层的,Dockerfile 里每一条 RUN、COPY、ADD 通常都会生成层。频繁构建时,新镜像和旧镜像共享部分层,但只要依赖变了、代码变了、构建步骤变了,就会产生新的层和缓存。
比如一个 Java 项目 Dockerfile 先 COPY 整个项目,再 mvn package,那么每次代码变化都可能让后面的依赖缓存失效。更合理的写法是先复制 pom.xml 下载依赖,再复制 src,这样依赖层可以复用,构建缓存更稳定。
Node.js 项目也类似。先 COPY package.json package-lock.json,再 npm ci,最后 COPY 业务代码。否则每次改一行代码,都可能重新安装依赖,镜像构建慢,缓存层也更容易堆。
减少镜像层和镜像体积
清理磁盘是一方面,镜像本身也要控制。常见优化包括合并 RUN 命令、清理包管理器缓存、使用 multi-stage build、避免把无关文件 COPY 进镜像。
例如 Debian/Ubuntu 镜像里安装软件后,可以清理 apt 缓存:
RUN apt-get update && apt-get install -y curl ca-certificates && rm -rf /var/lib/apt/lists/*
Alpine 里使用 apk 可以这样:
RUN apk add --no-cache curl ca-certificates
Go 项目适合 multi-stage build,编译环境和运行环境分开。编译阶段用 golang 镜像,运行阶段用 alpine 或 distroless,只把最终二进制复制进去。一个几百 MB 的镜像,经常能压到几十 MB。
.dockerignore 也很关键。不要把 .git、node_modules、target、dist、日志文件、临时文件都送进 build context。build context 越大,构建越慢,缓存越乱。
系统盘太小也会放大问题
不少云服务器默认系统盘 40GB,跑普通 Web 服务够用,但放到 Docker 构建机、测试环境、镜像拉取频繁的节点上,就偏紧了。
如果是轻量应用,只跑一两个容器,40GB 系统盘还能接受;如果是 CI/CD、多个业务混跑、经常 docker build,建议系统盘至少 80GB 到 120GB 起。镜像仓库、数据库、对象存储这类服务更要单独挂数据盘。
如果你也在找这种适合跑 Docker、测试环境、海外业务节点的云服务器,可以看看129云。比如美国精品大宽带-C型是 8C、8G DDR4 ECC、120G SSD、1Gbps 峰值带宽,适合构建、拉镜像、跨境测试这类场景;香港精品CN2-A型是 2C、2G、40G SSD、25Mbps 峰值,更适合轻量容器服务;需要香港 CN2 精品线路和更高配置,可以看香港CN2大宽带-E型,16C、16G、系统盘 Lin30Win50、70G SSD 数据盘、50Mbps 峰值。需要确认线路和库存可以直接打 400-9177118。
把 Docker 数据目录迁到数据盘
如果系统盘已经偏小,长期方案通常是把 Docker data-root 放到数据盘。比如新挂载一块 /data 盘,然后把 Docker 数据目录迁过去。
先停 Docker:
systemctl stop docker
创建新目录:
mkdir -p /data/docker
同步原数据:
rsync -aHAXx /var/lib/docker/ /data/docker/
修改 /etc/docker/daemon.json:
{ "data-root": "/data/docker", "log-driver": "json-file", "log-opts": { "max-size": "100m", "max-file": "3" } }
启动 Docker:
systemctl start docker
确认容器正常:
docker ps
确认 Docker Root Dir:
docker info | grep "Docker Root Dir"
确认没问题后,再考虑备份或删除旧的 /var/lib/docker。这里不要急着删,至少观察一段时间,确认容器、镜像、volume 都正常。
overlay2 目录很大但 prune 没效果怎么办
有时候 docker system df 显示没多少可回收,但 du 看到 overlay2 很大。可能有几种情况。
一种是正在运行的容器写入了大量数据。比如应用把上传文件、临时文件、缓存写在容器内部,而不是挂载到宿主机目录或 volume。查看容器可写层大小:
docker ps -s
如果某个容器 SIZE 很大,就要进容器看具体目录:
docker exec -it CONTAINER_ID sh
或者用 docker diff 看容器内文件变化:
docker diff CONTAINER_ID
另一种是文件被删除但进程仍占用句柄,df 不释放,du 又看不准。可以用 lsof 查 deleted 文件:
lsof | grep deleted
如果看到某个进程占着很大的 deleted 文件,需要重启对应进程或容器。不要盲目重启整台机器,先定位谁占着。
定期清理可以交给 cron,但命令别太狠
测试环境可以加定时任务,比如每周清理停止容器、dangling image、构建缓存:
0 3 * * 0 docker container prune -f && docker image prune -f && docker builder prune -f
如果是 CI 构建机,可以按时间清理 BuildKit 缓存:
docker builder prune -f --filter "until=168h"
这个意思是清理 168 小时以前的缓存,也就是 7 天前。比直接 prune -a 温和一些。
镜像也可以按业务规则处理,比如保留最近几个 tag。不要只按创建时间全删,因为有些稳定版本虽然老,但可能还在作为回滚基线。
监控要盯 /var/lib/docker 和 inode
云盘空间耗尽不一定只是容量满,inode 满也会导致无法创建文件。小文件特别多的构建缓存、npm 缓存、日志碎片,都可能把 inode 打满。
看 inode:
df -ih
监控里建议单独加几个指标:根分区使用率、inode 使用率、/var/lib/docker 目录大小、Docker 日志文件大小、容器数量、镜像数量。
报警阈值可以设置得实际一点。比如系统盘 80% 告警,90% 严重告警;inode 80% 告警;单个 json log 超过 1GB 告警。不要等到 100% 再处理,100% 时很多命令都会变得不好用。
现场常用的一组排查命令
df -h
df -ih
du -h --max-depth=1 /var/lib/docker | sort -hr
docker system df -v
docker ps -a
docker images
docker builder du
find /var/lib/docker/containers -name "*-json.log" -size +1G -ls
du -h --max-depth=1 /var/lib/docker/volumes | sort -hr | head
lsof | grep deleted
清理时容易踩的坑
不要手工删除 overlay2 里的目录。看起来能释放空间,实际可能破坏 Docker 元数据。
不要在没确认的情况下执行 docker volume prune。volume 里经常是真数据,不是缓存。
不要把 docker system prune -a --volumes 当作日常清理脚本。它适合非常明确的临时环境,不适合生产业务机。
不要只清理镜像,不处理日志限制。日志还会继续涨,过几天同样报警。
不要让构建机和生产运行节点混在一起。构建缓存、临时镜像、运行数据放在同一台机器上,排障时边界很乱。
不要忽略镜像仓库访问速度。清理旧镜像前要考虑回滚时能不能快速 pull 回来,尤其是跨境业务、海外节点、CN2/GIA 线路差异明显的环境。
一个比较贴近现场的处理过程
某台测试服务器系统盘 50GB,Docker 跑了十几个服务,同时还承担构建任务。报警显示 / 分区 98%。
先看 df -h,确认根分区快满。再看 du -h --max-depth=1 /var/lib/docker,发现 overlay2 26GB,containers 11GB,buildkit 8GB。
containers 里进一步 find,发现两个容器 json.log 分别 6GB 和 3GB。先 truncate 清空日志,空间立刻回来 9GB 左右。
接着 docker container prune 清掉一批 Exited 容器,释放 1GB 多。docker image prune 清 dangling image,释放 4GB。docker builder prune 清构建缓存,释放 6GB。
最后把 daemon.json 加上 max-size 和 max-file,避免日志继续无限涨。构建任务迁到另一台 120GB SSD 的机器上,业务运行节点只保留运行镜像,不再本机 build。
这个过程里没有动 volume,也没有手工删 overlay2。空间从 98% 降到 54%,容器没有重建,业务也没丢数据。
Dockerfile 和发布流程也要跟着改
如果清理完很快又满,问题一般不在清理命令,而在发布流程。比如每次发版都 build 一个新 tag,但旧 tag 永远不删;或者每个分支都在同一台机器构建;或者应用日志直接打 stdout,每秒几百行。
可以把镜像 tag 规范化,比如 app:commit-id、app:release-20240601,不要大量 latest、test、new、bak 这种不可追踪 tag。
CI 里构建成功后推到镜像仓库,运行节点只 pull 指定版本,不在运行节点 docker build。这样运行节点磁盘压力会小很多。
应用日志该进日志系统就进日志系统,stdout 可以保留关键日志,但不要把 debug 级别常态打开。容器平台不是日志仓库,本地 json log 只是兜底。
清理命令可以这样分环境使用
开发测试机:可以定期执行 docker container prune -f、docker image prune -f、docker builder prune -f。确认无重要 volume 的情况下,再考虑 docker volume prune。
CI 构建机:重点清 build cache 和 dangling image,可以用时间过滤,比如保留 7 天缓存。系统盘建议大一些,或者把 Docker data-root 放到独立数据盘。
生产运行节点:优先限制日志大小,谨慎删除镜像,保留可回滚版本。volume 只做人工确认后的定点清理。
临时压测机:压测结束后可以 docker system prune -a,确认数据无价值时再加 --volumes。
几个命令的推荐执行顺序
df -h
du -h --max-depth=1 /var/lib/docker | sort -hr
find /var/lib/docker/containers -name "*-json.log" -size +1G -ls
find /var/lib/docker/containers -name "*-json.log" -exec truncate -s 0 {} \;
docker container prune
docker image prune
docker builder prune
docker system df -v
docker ps -s
docker volume ls
这套顺序的好处是先处理最容易膨胀、风险最低的日志和停止容器,再处理镜像和构建缓存,最后才看 volume 和容器可写层。生产环境里,慢一点确认比删错数据强得多。