Tao
Tao

Docker prune:从入门到精通

Docker虽然好用,但也带来了隐形成本:随着时间推移,镜像、容器、卷和网络会不断堆积,悄悄吃掉大量磁盘空间。这并不是Docker的设计缺陷,而是其核心设计理念的结果。Docker 在管理资源时天生就比较"保守",它不会擅自删除任何东西,这就把清理系统的"烂摊子"甩给了用户。如果不主动清理,你的服务器硬盘很快就会被撑满。

理解哪些东西最占空间是有效清理空间的第一步。以下是五个主要的"垃圾"来源:

  1. 已停止的容器 (Stopped Containers):
    每次运行容器(即使只是一次性命令),Docker 都会创建一个容器实例。这些容器在停止后并不会自动消失,它们依然保留着自己的文件系统层,占用着空间。

  2. 悬空镜像 (Dangling Images):
    这是最常见的空间浪费源。当你重新构建一个已存在的镜像时(例如,docker build -t my-app .),旧版本的镜像并不会被删除,而是会失去它的标签,变成一个"悬空"或"没人要"的镜像,在列表中显示为 <none>:<none>。这些镜像不再被任何容器使用,纯粹是历史遗留问题。

  3. 未使用的镜像 (Unused Images):
    这比悬空镜像的范围更广。一个镜像可能有一个响亮的标签(如 ubuntu:20.04),但如果没有任何容器(无论是运行中还是已停止的)在使用它,那它就是一个未使用的镜像。你可能下载了很多基础镜像或测试镜像,用完后就忘了删。

  4. 未使用的卷 (Unused Volumes):
    卷是用来持久化存储数据的。但如果你创建的容器使用了卷,后来又把容器删了,那个卷并不会被自动删除。特别是"匿名卷"(没有名字的卷),它们很容易被遗忘,像数字世界的野草一样疯长。

  5. 未使用的网络 (Unused Networks):
    与卷类似,当你为特定应用创建了自定义网络,但之后删除了所有使用该网络的容器时,这个网络本身并不会消失。虽然单个网络占用的空间不大,但日积月累也很烦人。

不定期清理这些用不上的东西,不仅能释放大量磁盘空间,还能让你的Docker管理更方便,避免在查找和管理时像"翻箱倒柜找东西"一样困难。

为了解决上述问题,Docker 提供了一套强大的 prune(意为"修剪")命令。这些命令就像瑞士军刀,每把刀都有特定的用途。

这是清理工具里最厉害的"大扫除"工具,也是最常用的。它会一次性删除所有以下未使用的资源:

  • 所有已停止的容器
  • 所有悬空镜像
  • 所有未使用的网络
  • 所有构建缓存

默认情况下,docker system prune 不会删除未使用的卷,这是一个非常重要的安全设计,防止你一不小心把重要数据给删了。

bash

# 执行一次全面的系统清理
docker system prune

# 加上 -a 或 --all 参数,清理得更"狠",会删除所有未使用的镜像(不只是悬空的)
docker system prune -a

# 加上 --volumes 参数,会连同未使用的卷一起删除
docker system prune --volumes

警告: docker system prune --volumes 是一个危险的操作。它会删除所有没有被任何容器引用的卷,包括那些你可能还想保留的、存有重要数据的命名卷。使用前请三思。

如果你不想用"大扫除"命令,只想清理某一种特定的资源,可以使用下面这些更精细的命令。这样做的好处是目标明确,能减少误删的风险。

只删除所有已停止的容器。

bash

docker container prune

默认只删除悬空镜像 (<none>:<none>)。

bash

# 只删除悬空镜像
docker image prune

# 使用 -a 或 --all 参数,删除所有未被任何容器使用的镜像(包括有标签的)
docker image prune -a

只删除未使用的卷。

bash

docker volume prune

再次警告: 这个命令非常危险,删了就真没了。Docker 的设计思路是"数据至上",所以它默认不会碰你的卷。在你运行这个命令之前,请确保你真的知道自己在做什么,并且已经备份了重要数据。

只删除所有未使用的网络。

bash

docker network prune

Docker 的资源之间存在依赖关系:容器依赖镜像,卷和网络被容器使用。因此,一个安全的清理顺序应该是:

  1. 停止并删除容器 (docker stop / docker rmdocker container prune)。
  2. 删除不再需要的镜像 (docker image prune)。
  3. 删除不再需要的卷 (docker volume prune)。
  4. 删除不再需要的网络 (docker network prune)。

docker system prune 基本上就是帮你按这个顺序自动执行了一遍。

仅仅知道基础命令是不够的,要成为 Docker 清理大师,你必须掌握那些能让你精准控制的参数。

--all (-a) 和 --volumesdocker prune 命令里最有杀伤力也最容易被搞错的参数。

  • --all (-a):
    这个参数用在 docker image prune 上时,会将清理范围从"悬空镜像"扩大到"所有未使用的镜像”。这意味着,即使一个镜像有名字(比如 nginx:latest),只要没有任何容器在用它,它就会被删除。这对于清理那些拉下来测试后就没再用过的镜像非常有用。

  • --volumes:
    这个参数专属于 docker system prune。它就像一个开关,一旦打开,system prune 就会在清理时把未使用的卷也一并带走。再次强调,这可能会导致数据丢失。

  • --force (-f):
    这个参数会跳过"你确定吗?“的确认步骤,直接执行删除操作。它在自动化脚本里很有用,但手动操作时要格外小心,因为你连后悔的机会都没有。

--filter 参数让 docker prune 命令从一个"大锤"变成了一把"手术刀”。你可以用它来设定非常具体的规则,只删除符合特定条件的资源。

最常用的过滤器是 untillabel

你可以删除某个时间点之前创建的资源,until 的值可以是一个时间戳,也可以是一个时间段(比如 24h)。

bash

# 删除所有已停止超过24小时的容器
docker container prune --filter "until=24h"

# 删除所有超过10天未使用的镜像
docker image prune -a --filter "until=240h"

# 删除所有超过7天未使用的构建缓存
docker builder prune --filter "until=168h"

这是最强大的保护机制。你可以给不想被误删的资源打上一个特殊的"保护"标签,然后在清理时告诉 Docker “不要动有这个标签的东西”。

这种"排除法"的思路是一个巨大的转变。它让你从"看到什么删什么"的模式,变成了"没贴标签的才删"的模式,大大提高了安全性。

bash

# 1. 创建一个带保护标签的卷
docker volume create --label keep=true important_data

# 2. 删除所有没有 "keep=true" 标签的未使用卷
#    注意 "label!=key=value" 这个语法
docker volume prune --filter "label!=keep=true"

# 3. 保护镜像也是同理
docker tag my-image my-image:latest
docker label "my-image:latest" keep="true" # 注意:Docker 没有直接给镜像打标签的命令,这通常在构建时用 LABEL 指令完成
docker image prune -a --filter "label!=keep=true"

掌握了过滤器,你就可以放心地在自动化脚本中使用 docker prune -f 了,因为你知道它只会删除那些你明确允许删除的东西。

知道命令怎么用是一回事,知道什么时候用、怎么安全地用是另一回事。

在执行任何 prune 命令之前,特别是带 --force 的时候,请务必过一遍这个清单:

  1. 先大概看看情况:

    • docker ps -a:看看有哪些容器,它们的状态是什么。
    • docker images -a:看看你有哪些镜像,哪些是悬空的。
    • docker volume ls:看看有哪些卷,特别是那些名字很奇怪的匿名卷。
    • docker network ls:看看有哪些网络。
  2. 手动检查一下:
    对于那些你不确定的资源,比如一个看起来像是重要数据的卷,用 docker volume inspect <volume_name> 检查一下它的详细信息。

  3. 先演练,再执行:
    在不加 --force 的情况下运行命令,看看 Docker 提示将要删除哪些东西。确认无误后,再用 --force 执行(如果需要的话)。

  4. 备份!备份!备份!
    对于生产环境或者任何存有重要数据的卷,删除前一定要先备份。删了就真的找不回来了。

在开发环境里,你可以大胆地用 docker system prune -a --volumes 来释放空间。但在生产环境里,这么做无异于玩火。生产环境的核心原则是"稳定压倒一切"。

警告: 不要在生产服务器上随意运行 docker system prune,尤其是在你不完全清楚服务器上运行着哪些应用和数据时。

生产环境的清理策略应该是:

  • 宁可一个个删,不要一锅端: 尽量使用更精确的 container pruneimage prune 等命令,并且配合 --filter 使用。
  • 自动化要格外小心: 如果你必须在生产环境自动化清理,一定要使用带有"保护"标签的过滤器,确保核心应用和数据绝对不会被误删。
  • 理解应用,而不是依赖工具: docker prune 是一个一根筋的工具,它只看资源"有没有被引用",而不管这个资源对你的业务是否重要。清理前,你必须从应用的层面去理解每个卷、每个镜像的作用。

CI/CD 环境是 Docker 产生垃圾的重灾区。每次构建、每次测试都会产生大量的临时镜像和容器,如果不加以控制,CI/CD 服务器的磁盘很快就会爆炸。

一个典型的 CI/CD 流程可能包括:

  1. 拉取最新的代码。
  2. 使用 Dockerfile 构建一个新镜像。
  3. 用这个新镜像启动容器来跑测试。
  4. 测试通过后,将镜像推送到仓库。

在这个过程中,至少会留下一个悬空镜像(旧版本的镜像)和一个已停止的容器。日积月累,数量惊人。因此,在 CI/CD 流水线的末尾加入清理步骤至关重要。

在 GitLab CI/CD 中,你可以在 .gitlab-ci.yml 文件的 after_script 部分加入清理命令。after_script 里的命令无论流水线成功还是失败都会执行,非常适合做清理工作。

yaml

stages:
  - build
  - test
  - cleanup

build_job:
  stage: build
  script:
    - docker build -t my-app:$CI_COMMIT_SHA .
    - docker push my-app:$CI_COMMIT_SHA

test_job:
  stage: test
  script:
    - docker run my-app:$CI_COMMIT_SHA npm test

cleanup_job:
  stage: cleanup
  script:
    # 只清理超过1小时的资源,给并行任务留出足够的时间
    - docker system prune -a -f --filter "until=1h"
    # 为了更安全,也可以只清理悬空镜像和已停止的容器
    # - docker image prune -f
    # - docker container prune -f
  # 确保这个任务总会运行
  when: always

注意: 使用 until 过滤器是一个好习惯,可以避免清理掉那些可能还在被其他并行任务使用的资源。

docker system prune 会清除构建缓存,这意味着下次构建会从零开始,速度会变慢。这是一个典型的"性能"与"干净"之间的权衡。

  • 要性能: 保留构建缓存,加快构建速度。但磁盘占用会持续增长。
  • 要干净: 每次都清理构建缓存 (docker builder prune)。磁盘占用低,但每次构建都慢得像第一次。

最好的折中办法是定期清理,而不是每次都清理。例如,你可以设置一个每周运行一次的定时任务,专门用来清理旧的构建缓存。

bash

# 只删除超过一周未使用的构建缓存
docker builder prune --filter "until=168h"

docker-compose 用户经常会纠结:我该用 docker-compose down 还是 docker system prune?它们有很大不同。

特性 docker-compose down docker system prune
范围 项目范围:只管理由当前 docker-compose.yml 文件定义和创建的资源。 系统范围:管理宿主机上所有的 Docker 资源,不管它们是怎么创建的。
默认行为 - 删除容器
- 删除网络
- 删除已停止的容器
- 删除悬空镜像
- 删除未使用的网络
卷的处理 默认不删除卷。需要加 -v--volumes 参数才会删除。 默认不删除卷。需要加 --volumes 参数才会删除。
镜像的处理 默认不删除镜像。需要加 --rmi all (删除所有) 或 --rmi local (只删除没有自定义标签的) 参数才会删除。 默认只删除悬空镜像。加 -a 才删除所有未使用的。

一个典型的作死现场:
你有一个用 docker-compose 启动的重要应用(比如数据库),它使用了一个命名卷 my_db_data。后来你为了清理磁盘,在另一个项目目录或者根目录下运行了 docker system prune --volumes。由于 docker-compose 的容器当时可能已经停止了,prune 命令会认为 my_db_data 是一个"未使用"的卷,然后毫不留情地把它删掉。

你得这么想:

  • docker-compose down 是用来拆除一个特定应用的。
  • docker system prune 是用来给整个系统做大扫除的。

它们的目的完全不同,绝对不能混用。

这是最常见的问题:“我运行了 prune,但 df -h 显示磁盘空间没啥变化!”

最常见的原因是:

  • Docker 守护进程仍在占用文件句柄: 有时,即使资源被删除了,Docker 守护进程可能还没有释放对文件的锁定。重启 Docker 守护进程通常能解决问题 (systemctl restart docker)。
  • 真正占空间的东西没被删: prune 默认不删卷和有标签的镜像。也许你真正需要的是 docker system prune -a --volumes
  • “看不见的"被占空间: 真正占用大量空间的是 Docker 的 overlay2 目录(通常在 /var/lib/docker/overlay2)。即使你删除了容器和镜像,一些底层的存储层可能因为某些原因没有被正确清理。重启 Docker 或在极端情况下清理整个 Docker 目录(非常危险!)可能是最后的手段。

你确定一个容器已经停止,一个镜像没被使用,但 prune 就是不删它。

原因可能是:

  • 还有东西在用它: 你可能忽略了一个正在使用该资源的容器(即使是已停止的容器也会占用镜像和卷)。仔细用 docker ps -adocker volume inspect 检查。
  • 它是另一个镜像的基础: 一个镜像可能是另一个镜像的基础层(父镜像)。在这种情况下,除非你把所有依赖它的子镜像都删掉,否则它不会被删除。

当你有成千上万个 Docker 对象时,docker system prune 可能会运行得非常慢,甚至卡住。这是因为它需要把所有东西的关系都捋一遍,计算出哪些是可以安全删除的,这是一个非常耗费 I/O 的操作。

解决方法:

  • 分步清理: 不要用 system prune,而是分别运行 container prune, image prune 等。
  • 使用过滤器: until 过滤器可以显著减少需要检查的对象数量,从而加快速度。
  • 定期清理: 不要等到系统里有几万个对象了才想起来清理。养成定期清理的习惯,每次只处理少量对象,速度会快很多。

docker prune 是一个必不可少的工具,但它也是一把双刃剑。Docker 天生就"保守"的设计,决定了系统维护的责任最终落在了用户自己身上。

  • 默认保平安: prune 命令的默认行为是相对安全的,它不会动你的数据(卷)和有用的镜像。
  • 危险参数要警惕: --all--volumes 会大大增加清理的力度,也带来了数据丢失的风险。
  • 过滤器是你的安全网: 学会使用 labeluntil 过滤器,可以让你在享受自动化便利的同时,保证核心资源的安全。
  • 环境决定策略: 开发环境可以大刀阔斧,生产环境必须谨小慎微。
  • 清理是一项必须要做的工作,而不是可选项: 忽略 Docker 的清理工作,最终会导致磁盘耗尽、性能下降和管理混乱。

通用法则是:在不确定的时候,不要删。在删除之前,先检查。对于重要数据,永远要备份。

相关内容