Docker 基础体系 · 第 66/70 篇。示例以现代 Docker Engine、BuildKit 与 Compose 规范为基础,并明确 Linux 容器边界。

Docker 磁盘治理:Layer、Build Cache、Volume、日志和安全清理

Docker 磁盘占用并不是一个单一目录或一个单一对象造成的。一个正在运行的容器,至少可能同时涉及:

  • 镜像的只读 Layer;
  • 容器自身的可写 Layer;
  • BuildKit 产生的构建缓存;
  • Volume 或 bind mount 中的业务数据;
  • 容器日志;
  • Docker 网络、元数据以及临时文件。

这些对象的生命周期不同,删除条件也不同。删除镜像不一定会删除容器数据,删除容器也不一定会删除 Volume,清理 Build Cache 更不会自动清理日志。磁盘治理的第一原则是:先确认空间属于哪一种对象,再选择与其生命周期匹配的回收动作。


一、先建立磁盘空间模型

在 Linux 上,Docker Engine 通常由 dockerd 管理一个数据根目录。可以用以下命令查看:

docker info --format '{{.DockerRootDir}}'

典型输出可能是:

/var/lib/docker

Rootless Docker 的路径通常位于用户目录下,例如:

/home/alice/.local/share/docker

具体路径由安装方式、Rootless 配置和 Docker 版本决定,不能假设所有机器都是 /var/lib/docker

从逻辑上看,Docker 主机上的占用可以近似表示为:

DDocker=Dimage-layer+Dcontainer-writable+Dbuild-cache+Dvolume+Dlog+DmetadataD_{\text{Docker}} = D_{\text{image-layer}} + D_{\text{container-writable}} + D_{\text{build-cache}} + D_{\text{volume}} + D_{\text{log}} + D_{\text{metadata}}

其中:

  • Dimage-layerD_{\text{image-layer}}:镜像的只读层;
  • Dcontainer-writableD_{\text{container-writable}}:容器可写层;
  • Dbuild-cacheD_{\text{build-cache}}:BuildKit 的中间层、缓存挂载和构建结果;
  • DvolumeD_{\text{volume}}:Docker Volume 使用的独立存储;
  • DlogD_{\text{log}}:容器标准输出和标准错误日志;
  • DmetadataD_{\text{metadata}}:容器、网络、镜像索引等管理数据。

这个公式不是 Docker 对外承诺的精确账单,而是诊断时的分类模型。不同对象可能共享底层数据块,因此把各项目录大小简单相加可能重复计算。

查看 Docker 能识别的主要对象占用:

docker system df

输出通常类似:

TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          12        4         4.8GB     2.1GB (43%)
Containers      8         3         320MB     100MB (31%)
Local Volumes   6         2         18.4GB    12.0GB (65%)
Build Cache     27        0         6.7GB     6.7GB

更详细地查看镜像和容器:

docker system df -v

需要注意两个边界:

  1. docker system df 主要统计 Docker 管理的镜像、容器、Volume 和构建缓存;
  2. 它通常不会把日志作为一个独立的、完整的可回收类别展示。

因此,如果 docker system df 显示占用不高,但主机 df -h 已经接近满,日志、bind mount、文件系统保留空间、已删除但仍被进程打开的文件,都是重点怀疑对象。


二、Layer:镜像层、容器可写层和 Copy-on-Write

1. 镜像 Layer 是什么

Docker 镜像通常由多个只读 Layer 叠加组成。例如:

应用容器
├── 容器可写层
├── 应用依赖层
├── 系统工具层
└── 基础发行版层

Dockerfile 中很多会改变文件系统的指令,可能形成新的镜像层:

FROM debian:bookworm
RUN apt-get update && apt-get install -y curl
COPY app /opt/app

镜像层具有以下重要性质:

  • 通常是不可变的;
  • 可以被多个镜像共享;
  • 可以被多个容器共同引用;
  • 删除一个标签不等于删除所有对应 Layer;
  • 只有当某个 Layer 不再被任何镜像或容器引用时,才可能回收其存储。

标签只是名称,例如:

myapp:latest
myapp:v1

两个标签可能指向同一个镜像对象,也可能指向不同对象。删除标签主要是删除引用,不一定立即释放 Layer。

2. overlay2 与 Copy-on-Write

在 Linux 上,现代 Docker Engine 常见的存储驱动是 overlay2。可以查看当前驱动:

docker info --format 'driver={{.Driver}} root={{.DockerRootDir}}'

overlay2 使用 OverlayFS 将多个目录合并成一个统一视图。简化表示如下:

merged  = 容器看到的统一文件系统
upperdir = 容器可写层
lowerdir = 一个或多个镜像只读层
workdir  = OverlayFS 工作目录

容器读取文件时,OverlayFS 先在 upperdir 查找,再向下查找 lowerdir。这就是“统一视图”。

当容器修改一个来自镜像层的文件时,会发生 Copy-on-Write,也就是“写时复制”:

  1. 文件原本只存在于某个只读镜像层;
  2. 容器执行写操作;
  3. OverlayFS 将文件复制到容器的可写层;
  4. 后续读写针对可写层中的副本进行;
  5. 原始镜像层仍保持不变。

例如,基础镜像中有一个 500 MB 的文件:

lowerdir/usr/share/data.bin = 500MB

容器只修改其中很小的一部分,实际使用的空间并不保证只增加修改部分。对普通文件而言,Copy-on-Write 可能需要把整个文件复制到 upperdir,随后再写入修改内容。于是容器可写层可能额外占用接近 500 MB。

这也是为什么“容器里只改了一行配置”并不一定意味着只增加几 KB。

3. 删除文件并不一定立即释放空间

如果容器删除镜像层中的文件,OverlayFS 通常通过 whiteout 等机制在上层记录“下层文件已被删除”:

lowerdir/etc/example.conf       存在
upperdir/中的 whiteout 标记      表示隐藏它
容器统一视图中的文件             不存在

但下层镜像本身没有被修改,文件内容仍然存在于镜像 Layer 中。删除容器内文件只能改变容器视图,不能压缩或改写镜像层。

因此:

docker exec <container> rm -rf /some/path

可能释放容器可写层中的数据,却不能释放该路径在镜像层中的空间。

4. 容器可写层不适合存放持久数据

容器可写层的生命周期通常绑定容器:

  • docker restart 不会删除可写层;
  • docker stop 不会删除可写层;
  • docker rm 会删除该容器的可写层;
  • docker compose down 通常会删除由 Compose 创建的容器,从而删除其可写层;
  • 重新创建容器会得到新的可写层。

因此,数据库、上传文件、队列数据等不应依赖容器可写层。它们应放到 Volume 或外部存储中。否则重新部署或清理容器时,数据可能随容器一同消失。


三、为什么 dudf 和 Docker 统计可能不一致

诊断时至少需要同时看文件系统块和 inode:

df -h /var/lib/docker
df -i /var/lib/docker

示例:

Filesystem      Size  Used Avail Use% Mounted on
/dev/nvme0n1p2  100G   92G  8.0G  92% /

Filesystem        Inodes   IUsed   IFree IUse% Mounted on
/dev/nvme0n1p2   6553600 6551000    2600  100% /

第二种情况表示字节空间可能尚未完全用尽,但 inode 已经耗尽。此时创建新文件仍可能失败:

No space left on device

1. dfdu 的含义不同

  • df 统计文件系统已经分配的块;
  • du 遍历目录,统计当前仍可见的文件;
  • 一个文件被删除但仍被进程打开时,df 仍会计入其空间,而 du 看不到它。

可以在 Linux 上检查已删除但仍被打开的文件:

sudo lsof +L1

如果输出中存在 Docker 日志或容器相关文件,说明删除目录或文件名并不能释放空间,必须让持有文件描述符的进程关闭或重新打开该文件。对 Docker 日志而言,通常应通过重启或重新创建容器解决,而不是直接操作活动日志文件。

2. 不要直接修改 overlay2 内部目录

/var/lib/docker/overlay2 的目录结构属于存储驱动的内部实现。直接执行:

sudo rm -rf /var/lib/docker/overlay2/...

可能造成:

  • 镜像元数据与实际 Layer 不一致;
  • 容器启动失败;
  • 共享 Layer 被误删;
  • Docker daemon 无法正常加载对象;
  • 数据恢复困难。

清理 Layer 必须使用 Docker 的对象引用关系进行回收,例如 docker image prunedocker system prune,不能把 overlay2 当作普通临时目录。


四、Build Cache:构建缓存和镜像 Layer 不是同一个概念

1. Dockerfile 缓存的基本过程

BuildKit 会为 Dockerfile 的步骤建立缓存。例如:

FROM node:22-bookworm
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

当第二次构建时:

  1. FROM 的基础镜像可以复用;
  2. COPY package*.json ./ 的输入未变,则该步骤可以复用;
  3. RUN npm ci 的前置状态未变,则可以复用;
  4. 如果最后的源码 COPY . . 发生变化,只会使后续步骤失效。

这解释了为什么依赖文件应先复制、源码后复制:源码变化不会自动使依赖安装步骤失效。

缓存命中依赖于构建步骤的输入、指令和前置文件系统状态。修改 Dockerfile 中某个早期步骤,可能导致后续缓存全部失效。

2. Build Cache 与最终镜像的区别

最终镜像中的 Layer 是镜像的一部分;Build Cache 还可能包含:

  • 尚未被最终镜像引用的中间结果;
  • 不同构建阶段的中间层;
  • RUN --mount=type=cache 创建的缓存目录;
  • 外部缓存导入或导出的记录;
  • 多平台构建产生的中间对象。

例如:

RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt

/root/.cache/pip 的缓存数据用于加速后续构建,但不会自动进入最终镜像 Layer。删除构建缓存通常不会破坏已经构建完成的镜像,只会使后续构建变慢。

查看构建缓存:

docker buildx du

某些环境也可以使用:

docker builder prune

或:

docker buildx prune

具体可用参数以当前版本的帮助信息为准:

docker buildx prune --help

按时间清理一个较旧的缓存示例:

docker buildx prune --filter 'until=168h'

这里 168h 表示七天。清理前先观察缓存占用:

docker buildx du --verbose

BuildKit 可能同时被多个构建任务使用。正常的 prune 会由 BuildKit 处理引用关系,但在持续构建的 CI 主机上,过于激进的清理会造成:

  • 缓存命中率下降;
  • 构建时间增加;
  • 网络下载和 CPU 消耗上升;
  • 并发构建的性能抖动。

因此,Build Cache 的合理目标不是“始终为零”,而是控制其上限并回收长期未使用的记录。

3. 构建机与运行机的清理策略不同

构建机通常重视缓存命中率,适合保留近期缓存并按时间或容量清理。运行机通常不负责频繁构建,长期积累的 Build Cache 更可能属于可回收对象。

如果镜像通过外部 registry 作为发布介质,删除本机 Build Cache 不会删除 registry 中已经推送的镜像;但如果本机没有可重建的上下文、凭据或缓存,清理后恢复构建能力仍可能需要重新下载依赖。


五、Volume:持久数据的生命周期和最大风险

1. Volume 绕过容器可写层

Docker Volume 是由 Docker 管理的独立数据对象。挂载后,Volume 内的数据不再写入容器的可写 Layer:

容器进程
   │
   ├── /app        -> 容器可写层
   └── /var/lib/db -> Docker Volume

因此,数据库写入 Volume 不会因为容器可写层被限制或删除而直接消失。删除容器通常不会自动删除 Volume,这正是 Volume 用于持久化的原因。

查看 Volume:

docker volume ls
docker volume inspect mydata

典型的 inspect 输出包含:

[
  {
    "Name": "mydata",
    "Driver": "local",
    "Mountpoint": "/var/lib/docker/volumes/mydata/_data",
    "Labels": {}
  }
]

Mountpoint 是宿主机路径,但应用应通过 Docker 挂载使用它,而不是直接绕过 Docker 修改内部目录。

2. Named Volume、Anonymous Volume 和 bind mount

Named Volume:

docker volume create app-data
docker run -d \
  --name app \
  -v app-data:/var/lib/app \
  myapp:1.0

Anonymous Volume:

docker run -d \
  --name app \
  -v /var/lib/app \
  myapp:1.0

匿名 Volume 的名字由 Docker 生成,容易在长期运行的环境中积累。

bind mount 则直接使用宿主机路径:

docker run -d \
  --name app \
  -v /srv/app-data:/var/lib/app \
  myapp:1.0

bind mount 不属于 Docker Volume,docker volume prune 不会清理它。其容量应使用宿主机工具诊断:

sudo du -xsh /srv/app-data

这三种方式不能混为一谈:

类型 数据位置 是否由 Docker Volume 管理 volume prune 是否处理
Named Volume Docker 数据目录或驱动后端 可能处理未使用对象
Anonymous Volume Docker 数据目录或驱动后端 可能处理未使用对象
bind mount 宿主机指定路径

3. “未使用”不等于“没有价值”

Docker 的 Volume 回收判断主要依据 Docker 对象引用关系,例如没有容器引用它。它无法知道:

  • Volume 中是否有备份;
  • 是否有审计数据;
  • 是否准备被下一个容器使用;
  • 应用是否只是暂时停止;
  • 数据是否已复制到外部系统。

因此,Volume 的“可回收”只表示“Docker 当前没有活跃引用”,不表示“业务上可以删除”。

列出当前容器的挂载关系:

docker ps -a \
  --format 'table {{.ID}}\t{{.Names}}\t{{.Mounts}}'

检查具体容器:

docker inspect app \
  --format '{{json .Mounts}}'

删除 Volume 前,应至少完成:

  1. 确认没有生产容器使用;
  2. 确认数据已备份或明确不需要;
  3. 确认恢复流程能够读取备份;
  4. 确认 Compose、systemd 或其他编排工具不会再次使用它。

六、容器日志:标准输出也会占满磁盘

1. 日志数据流

容器进程通常把日志写入标准输出和标准错误:

应用 stdout/stderr
        │
        ▼
Docker logging driver
        │
        ├── json-file
        ├── local
        ├── journald
        ├── syslog / fluentd / gelf ...
        └── 其他驱动

日志驱动决定 Docker 如何接收、格式化、保存或转发日志。日志驱动不等于应用日志框架;应用仍然负责日志级别、结构化字段和敏感信息控制。

常见的 json-file 驱动会把每条日志包装成 JSON。其底层文件通常位于 Docker 数据根目录下的容器目录中,但具体路径属于实现细节,不应写死在运维脚本里。

检查 daemon 默认日志驱动:

docker info --format '{{.LoggingDriver}}'

检查某个容器实际使用的日志驱动:

docker inspect app \
  --format '{{json .HostConfig.LogConfig}}'

2. 日志轮转配置

可以为 Docker daemon 配置默认日志驱动和轮转参数,例如 /etc/docker/daemon.json

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "100m",
    "max-file": "5"
  }
}

含义是单个日志文件达到约 100 MB 后轮转,最多保留 5 个文件。这里的总量大致上限是:

DlogNcontainer×Smax-size×Nmax-fileD_{\text{log}} \approx N_{\text{container}} \times S_{\text{max-size}} \times N_{\text{max-file}}

实际占用还会受到轮转时机、文件系统块分配和并发写入影响,所以这只是容量估算,不是精确保证。

修改 daemon 默认配置通常只影响之后创建的容器。已经存在的容器可能仍沿用创建时的日志配置,生产变更后应检查并按计划重新创建相关容器。

Compose 中可以为服务指定日志配置:

services:
  api:
    image: example/api:1.0
    logging:
      driver: json-file
      options:
        max-size: "100m"
        max-file: "5"

Compose 配置是否生效,取决于容器是否被重新创建。仅执行普通的 docker compose restart 通常不会改变容器创建时的日志驱动配置;需要在变更后执行适当的重建或重新创建流程。

3. 其他日志驱动的边界

如果使用 journald、syslog 或远程日志驱动,日志可能主要存放在 Docker 之外:

  • journald 由 systemd journal 的配额和轮转策略控制;
  • syslog 由宿主机 syslog 服务控制;
  • 远程驱动的本地缓存、网络失败重试和下游不可用仍可能产生本地占用;
  • 某些驱动不支持完整的 docker logs 行为。

不能只看 Docker 数据目录来判断所有日志空间。应同时检查日志驱动和宿主机日志系统的配额。

4. 紧急日志处理的风险

不建议直接删除正在写入的日志文件:

sudo rm /path/to/container-json.log

进程可能仍持有已删除文件的文件描述符,结果是:

  • df 仍显示空间没有释放;
  • du 看不到这个文件;
  • 日志记录状态异常;
  • 后续排障失去关键证据。

生产环境应优先使用日志轮转、限制日志级别、外部日志系统或重新创建容器。若磁盘已经接近耗尽,临时释放空间必须由有经验的管理员执行,并立即验证:

df -h /
sudo lsof +L1
docker logs --tail 20 app

紧急操作不能替代长期轮转配置。


七、安全清理:删除对象不等于安全擦除秘密

Docker 的常规 prune 是空间回收,不是安全擦除。

如果敏感信息曾经进入以下对象:

  • 镜像 Layer;
  • Build Cache;
  • Volume;
  • 容器日志;
  • bind mount;
  • registry 或备份;

即使删除标签、容器或缓存,底层数据也可能因为快照、备份、远端副本、文件系统回收策略而继续存在。

1. 不要在 Dockerfile 中泄露秘密

错误示例:

ARG NPM_TOKEN
RUN npm config set //registry.example.com/:_authToken=$NPM_TOKEN

风险在于秘密可能进入:

  • 构建命令记录;
  • 镜像历史;
  • 中间 Layer;
  • Build Cache;
  • CI 日志。

现代 BuildKit 支持使用 secret mount 的构建方式,例如:

DOCKER_BUILDKIT=1 docker build \
  --secret id=npmrc,src="$HOME/.npmrc" \
  -t example/app:1.0 .

Dockerfile:

# syntax=docker/dockerfile:1
FROM node:22-bookworm

WORKDIR /app
COPY package*.json ./

RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
    npm ci

秘密在该 RUN 步骤中以临时挂载方式提供,不应被复制到最终镜像。仍需注意:

  • 不要在命令中打印秘密;
  • 不要把秘密写入构建产物;
  • CI 日志可能记录失败命令或调试输出;
  • 需要根据当前 BuildKit 和 Docker 版本验证 secret mount 支持。

如果秘密已经进入镜像或日志,正确处理通常是立即轮换凭据,再清理受影响对象;仅执行 prune 不能证明秘密已经从所有副本中消失。

2. 安全删除的实际含义

在 SSD、虚拟磁盘、写时复制存储、快照和 RAID 环境中,普通 rm 或 Docker prune 无法保证物理介质上的不可恢复擦除。

如果要求真正降低数据恢复可能性,应根据数据所在介质和组织策略采用:

  • 磁盘或 Volume 加密;
  • 加密密钥销毁;
  • 云盘或存储后端提供的安全删除能力;
  • 受控销毁备份、快照和 registry 副本。

这属于存储安全和合规问题,不应把 Docker 垃圾回收命令当作安全擦除工具。


八、各类清理命令及其边界

1. 清理已停止的容器

docker container prune

它清理已停止的容器,不清理正在运行的容器。容器被删除后,其可写层也会被删除,但挂载的 Named Volume 通常不会因此自动删除。

带过滤条件的示例:

docker container prune --filter 'until=168h'

这里按 Docker 支持的时间过滤语义选择较旧对象。执行前应理解当前版本的过滤规则:

docker container prune --help

2. 清理悬空镜像

docker image prune

通常清理没有标签且不再被引用的镜像层。它比下面的命令保守:

docker image prune -a

-a 会扩大到所有未被现有容器使用的镜像。生产环境中,一个镜像可能当前没有容器引用,但仍是回滚版本或离线恢复所需对象,因此不能仅以“现在没运行”判断是否可删。

3. 清理未使用 Volume

docker volume prune

Volume 清理是高风险操作。不同 Docker 版本对默认行为和 -a 选项的支持可能存在差异,应先查看:

docker volume prune --help

在支持的版本中,-a 会扩大清理范围,可能包含未使用的 Named Volume:

docker volume prune -a

不要把以下两者混为一谈:

docker compose down
docker compose down -v

前者通常删除 Compose 创建的容器和网络,但保留声明的 Volume;后者会删除项目关联的非外部 Volume。external: true 的外部 Volume 通常不由 Compose 删除,但仍应以当前 Compose 版本和配置验证。

4. 清理构建缓存

docker buildx prune --filter 'until=168h'

或者使用传统 builder 接口:

docker builder prune --filter 'until=168h'

先看占用:

docker buildx du --verbose

构建缓存删除后,镜像通常仍可运行,但下一次构建需要重新执行被淘汰的步骤。

5. 组合清理命令的风险

docker system prune

通常会清理一组未使用对象,但默认不会删除 Volume。扩大范围:

docker system prune -a --volumes

这可能同时涉及:

  • 已停止容器;
  • 未使用网络;
  • 未使用镜像;
  • 构建缓存;
  • 未使用 Volume。

它不是“自动安全清理”,而是多个高影响动作的组合。生产环境不应把它放进没有审计、没有过滤条件、没有备份验证的定时任务中。


九、一个可审计的清理流程

下面是一套适合人工执行的流程。它的重点不是命令数量,而是每一步都保留判断证据。

第一步:确认哪个文件系统满了

df -hT
df -i

如果 /var/lib/docker 位于根分区,则应关注根分区;如果 Docker 根目录位于单独挂载点,则单独检查该挂载点。

第二步:确认 Docker 数据根目录和驱动

docker info --format \
'root={{.DockerRootDir}} driver={{.Driver}} logging={{.LoggingDriver}}'

这一步能避免误把另一块磁盘或 bind mount 当成 Docker 内部空间。

第三步:分类统计

docker system df
docker system df -v
docker ps -a --size
docker volume ls
docker buildx du

docker ps -a --size 可以辅助发现容器可写层增长异常,但不能代表 Volume 或日志占用。

第四步:定位宿主机目录

对 Docker 数据根目录执行只读分析:

sudo du -xhd1 "$(docker info --format '{{.DockerRootDir}}')" \
  2>/dev/null | sort -h

-x 避免跨越到其他文件系统。若发现 Docker 内部目录很大,应回到 Docker 对象层面确认,而不是直接删除目录。

同时检查 bind mount:

sudo du -xhd1 /srv 2>/dev/null | sort -h

第五步:先清理低风险对象

例如清理明确已停止且超过保留期的容器:

docker container prune --filter 'until=168h'

再按计划清理旧 Build Cache:

docker buildx prune --filter 'until=168h'

每完成一类清理,就重新确认:

df -h
docker system df

第六步:处理日志和 Volume

如果日志是主要占用,应修改轮转策略并重新创建受影响容器,而不是直接删除活动日志文件。

如果 Volume 是主要占用,应按业务数据清单逐个核对。可以先导出备份,例如对一个测试 Volume:

docker run --rm \
  -v app-data:/source:ro \
  -v "$PWD":/backup \
  alpine \
  tar czf /backup/app-data.tar.gz -C /source .

恢复测试:

docker volume create app-data-restore

docker run --rm \
  -v app-data-restore:/target \
  -v "$PWD":/backup \
  alpine \
  tar xzf /backup/app-data.tar.gz -C /target

这两个命令分别把 Volume 内容打包到宿主机当前目录,再解压到新 Volume。真实生产备份还应考虑一致性:数据库需要使用数据库自身的备份机制或在合适状态下执行快照,不能把正在变化的数据库目录简单打包就认为是可恢复备份。

第七步:清理后验证服务行为

空间释放后,还要验证:

docker ps
docker compose ps
docker logs --tail 50 <container>
df -h
df -i

如果之前发生过 inode 耗尽、数据库写满、日志丢失或容器重建,必须进一步检查应用健康状态、数据完整性和告警恢复情况。


十、自动化回收必须处理并发和故障路径

磁盘清理通常与以下操作并发发生:

  • CI 正在构建镜像;
  • 发布系统正在拉取镜像;
  • Compose 正在重建容器;
  • 应用正在写日志;
  • 备份任务正在读取 Volume;
  • 运维人员同时手动执行 prune。

Docker daemon 会维护对象引用关系,正常的 Docker 命令不会简单删除仍被运行容器引用的镜像层。但业务层面的竞态仍然存在。例如:

  1. 运维脚本判断某个 Volume 当前未被容器挂载;
  2. 部署系统在下一秒创建新容器并准备使用它;
  3. 清理脚本删除 Volume;
  4. 新容器启动时得到空数据目录或直接失败。

因此自动化流程应使用明确的标签、项目范围和保留策略,而不是依赖“当前看起来没有引用”。例如为临时构建资源设置标签,并在创建和清理时保持同一套命名规则。对于 Compose 项目,清理前应确认项目、外部 Volume 和部署控制器的实际关系。

一个安全的状态转换可以表示为:

flowchart TD
    A[发现磁盘告警] --> B[确认文件系统与 DockerRootDir]
    B --> C[区分 Layer / Cache / Volume / Log / bind mount]
    C --> D{是否有业务数据或活动引用?}
    D -- 是 --> E[停止写入或迁移数据]
    E --> F[备份并验证恢复]
    F --> G[按对象类型清理]
    D -- 否 --> G
    G --> H[重新检查 df / inode / system df]
    H --> I[验证容器、日志和应用健康状态]
    I --> J[记录清理结果与保留策略]

图中的“备份并验证恢复”不是所有镜像或缓存都需要执行,但对 Volume 和包含敏感信息的对象,不能省略业务判断。


十一、常见误解和对应的失败表现

误解一:删除容器就会删除所有数据

实际情况:

  • 容器可写层通常会删除;
  • Named Volume 通常保留;
  • bind mount 数据保留;
  • 镜像 Layer 保留;
  • 日志是否删除取决于容器日志文件及其生命周期。

因此,删除容器后磁盘没有明显下降并不矛盾。

误解二:删除镜像标签就释放了镜像空间

标签只是引用。只要其他标签、容器或缓存仍然引用相同 Layer,数据就不能回收。应通过:

docker system df -v

观察共享关系和可回收空间,而不是只看 docker image ls 的标签数量。

误解三:容器内 rm 可以清理镜像空间

容器内删除操作只改变容器视图,并可能增加 whiteout 元数据。它不能修改下层镜像。要缩小镜像,应修改 Dockerfile、减少不必要文件、在同一构建步骤中清理包管理器缓存,并重新构建镜像。

例如下面的写法容易把包索引保留在镜像层:

RUN apt-get update
RUN apt-get install -y curl

更合理的方式是让索引下载、安装和清理处于同一个 Layer:

RUN apt-get update \
 && apt-get install -y --no-install-recommends curl \
 && rm -rf /var/lib/apt/lists/*

这不是对所有发行版和软件包管理器都通用的固定模板,但其因果关系是明确的:只有在同一 Layer 中删除,删除结果才不会被前一个 Layer 的内容继续占用。

误解四:df 满了就清理镜像

如果真正占用空间的是:

  • /var/lib/docker/containers/... 下的日志;
  • bind mount;
  • Volume;
  • inode;
  • 已删除但仍打开的文件;

清理镜像可能几乎没有效果。必须先分类。

误解五:prune 就是安全擦除

prune 只解除 Docker 对象并回收可回收空间,不负责清除快照、备份、registry 副本或 SSD 上的物理残留。秘密一旦泄露,首要动作是凭据轮换,其次才是按范围清理和验证。


十二、生产环境中的容量告警边界

容量告警至少应覆盖三类指标:

  1. 文件系统字节使用率;
  2. inode 使用率;
  3. Docker 对象和应用数据增长率。

只监控 df -h 可能漏掉 inode 耗尽,只监控 docker system df 又可能漏掉 bind mount 和日志。

还应区分:

  • Docker 数据根目录所在文件系统;
  • Volume 所在的外部存储;
  • bind mount 所在文件系统;
  • journald 或远程日志系统;
  • registry、构建缓存和 CI 工作目录。

容量告警最好在“还有恢复余量”时触发,而不是等到写操作已经失败后才执行清理。告警内容应包含挂载点、使用率、inode 使用率、最近增长量和可能的最大目录,便于直接进入分类诊断。


结语

Docker 磁盘治理的关键不是记住某一条 prune 命令,而是理解对象之间的生命周期:

  • Layer 由镜像和容器引用关系决定;
  • 容器可写层随容器存在,但不适合持久化;
  • Build Cache 服务于构建速度,删除后通常影响性能而不是运行结果;
  • Volume 保存业务数据,Docker 判断“未使用”不代表业务上可以删除;
  • 日志由 logging driver 管理,轮转必须在容器创建配置和宿主机日志系统两侧确认;
  • 安全清理不等于 Docker 垃圾回收,秘密泄露需要凭据轮换和存储范围治理。

正确的操作顺序始终是:确认文件系统,分类对象,识别引用关系,备份业务数据,按风险执行清理,再验证空间和服务状态。


系列导航与关联阅读

官方资料

本文依据 Docker、OCI 与 CNCF 官方文档重新梳理;正文与生产检查清单由 WR BLOG 编写。