别再乱删镜像了!Docker磁盘空间清理终极指南(2024最新版)

你是否也经历过这样的场景:在一个紧张的项目迭代周期里,正准备拉取最新的基础镜像来构建服务,终端却无情地弹出了 no space left on device 的红色错误。团队 Slack 频道里立刻有人喊:“谁又把测试镜像堆满了服务器?” 你手忙脚乱地执行 docker system prune -a,看着一串串被删除的镜像 ID,心里既心疼又无奈,因为你知道,几周后这个问题还会卷土重来。对于长期与 Docker 打交道的开发者而言,磁盘空间告警就像房间里的大象,大家心照不宣,却很少有人系统地解决它。临时清理只是止痛片,我们需要的是从根源上理解 Docker 的存储机制,并建立一套可持续的维护习惯。这篇文章,就是为你准备的“根治”方案。

我们将超越简单的命令罗列,深入 Docker 存储驱动、镜像分层、卷管理的底层逻辑,提供从应急处理到长期架构优化的完整路径。无论你是管理着数十个微服务的团队负责人,还是需要在本地机器上高效开发的工程师,都能在这里找到贴合你场景的实践策略。

1. 理解 Docker 存储膨胀的根源:不只是镜像那么简单

当磁盘空间不足时,很多人的第一反应是“镜像太多了”。这没错,但只对了一部分。Docker 占用的空间是一个复杂的集合体,包括镜像(Images)、容器(Containers)、卷(Volumes)和构建缓存(Build Cache)。盲目删除镜像,可能误伤正在运行的容器依赖层,或者遗留大量无人问津的“僵尸”卷,它们才是真正的空间吞噬者。

首先,让我们用 Docker 自带的诊断工具,清晰地看到空间被谁吃掉了:

docker system df

这个命令会输出一个清晰的表格,类似下面这样:

TYPETOTALACTIVESIZERECLAIMABLE
Images451218.7GB13.2GB (70%)
Containers1532.1GB1.9GB (90%)
Local Volumes8525.4GB10.2GB (40%)
Build Cache--4.3GB4.3GB (100%)

从这个表里,你能立刻发现症结所在。也许你的镜像只占了 18GB,但那些早已停止的容器留下的可写层(在“Containers”里),以及几个用于数据库存储的本地卷,才是占用 25GB 的“大户”。镜像是只读的模板层,容器层是在镜像之上添加的可写层,而则是完全独立于容器生命周期的持久化数据存储。理解这三者的关系,是高效清理的前提。

注意:RECLAIMABLE 列非常关键,它表示可以安全清理的空间比例。对于 Images,它通常指那些没有被任何容器(包括已停止的)引用的“悬空”镜像。对于 Containers,则指已停止的容器所占空间。

镜像本身采用分层存储机制。当你拉取一个 ubuntu:22.04 镜像时,Docker 会下载多个只读层(如基础文件系统层、apt 安装层等)。当你基于它运行一个容器并安装 nginx 时,会在顶部添加一个薄薄的可写层。问题在于,当你 commit 这个容器生成新镜像,或者用不同 Dockerfile 反复构建时,会产生大量中间层和悬空镜像。它们像幽灵一样占据着空间。

2. 应急清理与日常维护:掌握正确的“修剪”艺术

面对告警,我们需要快速释放空间。但 docker system prune -a 是一把双刃剑,它会删除所有未被使用的镜像(包括可能下次要用到的),有时过于激进。我们应该像外科手术一样,进行精准清理。

2.1 分步清理,控制风险

首先,清理最安全的部分——构建缓存和悬空资源:

# 清理所有构建缓存,这通常很安全
docker builder prune

# 清理所有悬空镜像(未被任何镜像引用的中间层)
docker image prune

# 清理所有已停止的容器
docker container prune

# 清理所有未被使用的卷(务必先确认卷内无重要数据!)
docker volume prune

每个命令都可以加上 -f 来跳过确认提示,但在生产环境或首次操作时,建议先不加 -f,查看一下将要删除的内容列表。

如果你需要更细粒度的控制,可以使用过滤参数。例如,删除所有创建时间早于 48 小时的悬空镜像:

docker image prune --filter "until=48h"

2.2 高级清理策略与脚本化

对于团队环境,可以将清理工作自动化。但切记,自动化清理策略必须根据团队的实际工作流来定制。下面是一个示例脚本,它会在每周日凌晨清理超过 7 天的已停止容器和悬空镜像,但会保留最近使用的 10 个镜像标签:

#!/bin/bash
# cleanup_docker.sh - 保守的每周清理脚本

echo “开始 Docker 系统清理 $(date)”

# 1. 删除所有已停止的容器(超过7天)
docker container prune --force --filter “until=168h”

# 2. 删除所有悬空镜像
docker image prune --force

# 3. 删除所有未被容器引用的卷(危险操作,请根据实际情况注释)
# docker volume prune --force

# 4. 清理构建缓存
docker builder prune --force

# 5. 选择性清理镜像:保留最近10个使用的标签,删除其他未被容器引用的标签化镜像
# 此步骤较为复杂,通常需要结合镜像仓库API或第三方工具,此处仅提供思路。

echo “清理完成。当前磁盘使用情况:”
docker system df

将这个脚本加入服务器的 crontab 中。但请务必在测试环境充分验证,并确保团队所有成员都了解这个清理策略,避免重要的开发或测试镜像被误删。

提示:对于 CI/CD 服务器,可以配置在流水线任务完成后自动清理本次构建产生的所有临时容器和镜像,做到“随用随清”,这是最有效的预防策略。

3. 治本之策:迁移 Docker 根目录与存储驱动优化

当清理成为常态,你就该考虑“扩容”了。这里的扩容不是买新硬盘,而是将 Docker 的默认存储目录 /var/lib/docker 迁移到更大的磁盘分区上。网络上常见的方法是使用软链接,这在短期内有效,但并非官方推荐的最佳实践,尤其是在考虑高可用或后续维护时。

3.1 官方推荐方法:修改 Daemon 配置

更健壮的方式是直接修改 Docker 守护进程的配置,指定新的数据根目录。以下是针对使用 systemd 的 Linux 发行版的步骤:

  1. 停止 Docker 服务

    sudo systemctl stop docker
    

    如果使用了 Docker Compose 或 Kubernetes,请确保所有相关容器已停止。

  2. 移动现有数据(如果存在且需要保留)

    sudo mv /var/lib/docker /path/to/new/location/docker
    

    这里的 /path/to/new/location 应该是一个挂载了大容量磁盘的路径,例如 /data/mnt/big_disk

  3. 创建 systemd 配置覆盖文件: Docker 由 systemd 管理,我们可以通过创建 drop-in 文件来修改其启动参数。

    sudo mkdir -p /etc/systemd/system/docker.service.d
    sudo vim /etc/systemd/system/docker.service.d/override.conf
    

    在该文件中添加以下内容,使用 --data-root 参数指定新路径:

    [Service]
    ExecStart=
    ExecStart=/usr/bin/dockerd --data-root=/path/to/new/location/docker
    

    注意 ExecStart= 这一行是清空原有命令,必须存在。

  4. 重新加载 systemd 并启动 Docker

    sudo systemctl daemon-reload
    sudo systemctl start docker
    sudo systemctl enable docker
    
  5. 验证

    sudo docker info | grep “Docker Root Dir”
    

    输出应该显示新的路径。之后,你可以安全地删除旧的 /var/lib/docker 目录(如果它是空的或已移动)。

3.2 存储驱动选择:overlay2 的压倒性优势

Docker 的存储驱动决定了镜像和容器层如何在磁盘上组织和管理。不同的驱动对性能和磁盘空间利用率有显著影响。在主流现代 Linux 发行版上,overlay2 是默认且首选的驱动。

  • devicemapper (已弃用):旧版本 CentOS/RHEL 的默认驱动,性能较差,容易导致磁盘空间碎片化甚至耗尽,应尽快迁移。
  • overlay2:目前 Linux 内核的主流支持驱动,它利用联合文件系统,效率高,层共享性好,能有效节省空间。

检查你的存储驱动:

docker info | grep “Storage Driver”

如果显示不是 overlay2,并且你的内核支持(内核版本 >= 4.0,或 RHEL/CentOS 内核版本 >= 3.10.0-693),强烈建议切换。切换存储驱动需要重新初始化 Docker 数据目录,这意味着所有现有镜像、容器、卷都会丢失,务必先在非生产环境操作并备份重要数据。

切换步骤大致如下:

  1. 停止 Docker,备份 /var/lib/docker(如果需要)。
  2. 编辑 /etc/docker/daemon.json(如果不存在则创建):
    {
      “storage-driver”: “overlay2”
    }
    
  3. 彻底移除旧的 Docker 数据目录:sudo rm -rf /var/lib/docker
  4. 启动 Docker 服务。此时 Docker 会使用新的 overlay2 驱动初始化一个干净的数据目录。

4. 构建习惯与架构预防:让空间管理成为本能

清理和迁移是“战术后仰”,而良好的习惯和架构设计才是“战略俯视”。以下是一些能让你的 Docker 环境长期保持清爽的最佳实践。

4.1 镜像构建优化:写出“苗条”的 Dockerfile

一个臃肿的镜像是万恶之源。优化 Dockerfile 能从源头减少空间占用。

  • 使用多阶段构建:这是减少镜像体积的核武器。它允许你在一个 Dockerfile 中使用多个 FROM 语句,将编译环境和运行时环境分离。

    # 第一阶段:构建环境
    FROM golang:1.19 AS builder
    WORKDIR /app
    COPY . .
    RUN go build -o myapp .
    
    # 第二阶段:运行环境
    FROM alpine:latest
    WORKDIR /root/
    COPY --from=builder /app/myapp .
    CMD [“./myapp”]
    

    最终镜像只包含小巧的 Alpine Linux 和编译好的二进制文件,而不包含整个 Go 工具链,体积可能从 1GB 缩减到 10MB。

  • 合并 RUN 指令,并清理缓存:每一行 RUN 都会创建一个新的镜像层。将相关命令合并,并在同一层中清理 apt 或 yum 缓存。

    # 不佳的做法
    RUN apt-get update
    RUN apt-get install -y package1
    RUN apt-get install -y package2
    RUN rm -rf /var/lib/apt/lists/*
    
    # 推荐的做法
    RUN apt-get update && \
        apt-get install -y package1 package2 && \
        rm -rf /var/lib/apt/lists/*
    
  • 使用 .dockerignore 文件:防止将本地不必要的文件(如 .gitnode_modules, 日志文件)复制到构建上下文中,这能加速构建并避免意外增加镜像大小。

4.2 基础设施即代码与镜像仓库策略

  • 使用私有镜像仓库并设置保留策略:无论是 Harbor、GitLab Container Registry 还是 AWS ECR,都支持设置镜像保留规则,例如“仅保留最近 10 个 dev-* 标签的镜像”或“自动清理 90 天前的镜像”。将清理压力从开发者的本地和测试服务器转移到仓库,实现集中化管理。

  • 将 Docker 数据目录放在独立分区:在最初规划服务器时,就将 /var/lib/docker 或自定义的数据根目录挂载到独立的大容量磁盘分区。这样即使 Docker 写满,也不会影响操作系统关键分区(如 //var)的正常运行。

  • 监控与告警:使用 Prometheus、Grafana 等工具监控 Docker 宿主机磁盘使用率,并设置告警规则(例如,当 Docker 数据目录使用率超过 80% 时触发)。变被动清理为主动预防。

最后,分享一个我亲身经历的教训。曾经我们团队的一个测试服务器因为无人维护,/var/lib/docker/overlay2 目录增长到数百 GB,其中绝大部分是数千个停止状态的容器层。我们写了一个脚本,不是简单地 prune,而是分析了每个容器层的最后使用时间,并与 CI 系统的构建记录关联,最终发现是某个旧版本的构建脚本在出错时没有正确清理容器。修复那个脚本后,问题再也没有出现。所以,真正的“终极指南”不是一堆命令,而是培养一种意识:像对待你的代码仓库一样,对待你的 Docker 环境,保持它的整洁和可追溯性。当你下次再看到 no space left on device 时,希望你能从容地打开监控图表,然后执行一个早已安排好的维护任务,而不是慌乱地开始搜索清理命令。

更多推荐