docker image ls只告诉你「有什么」,不告诉你「在哪」。真出问题时------磁盘满了、镜像层找不到、pull 卡住不动------必须能顺着目录一路找到文件本身。这篇把/var/lib/docker的两区结构、overlay2 的四个子目录、从镜像名到磁盘目录的四级映射讲清,再补上daemon.json里那几个决定「能不能拉、能不能被连」的字段。
本文概要:
| 项目 | 内容 |
|---|---|
| 文章主题 | Docker 镜像在本地的存储结构(元数据区与层数据区、overlay2 四目录、四级映射链)、空间清理的边界,以及 daemon.json 与 systemd 覆盖的配合 |
| 所属类别 | 云计算 → 容器技术 → Docker 存储与守护进程配置 |
| 知识关键字 | /var/lib/docker · overlay2 · cache-id · repositories.json · daemon.json · ExecStart · containerd image store |
主线分两半。前半是「文件在哪」 :目录结构加映射链,用来定位和排查。后半是「配置怎么生效」:daemon.json 与 systemd 覆盖,用来解决拉不动、连不上。
两半的接点在 docker info:它一次就把存储驱动、镜像根目录、仓库配置全告诉你。
一、两个区:一个记索引,一个存文件
1.1 先确认存储驱动
镜像都放在 /var/lib/docker/ 下,但具体怎么组织由存储驱动(Storage Driver)决定 。StorageDriver 不是一个真实目录名,它是变量------实际会被替换成 overlay2、vfs、btrfs 之类。所以第一步永远是:
bash
[root@docker ~]# docker info | grep "Storage Driver"
Storage Driver: overlay2
绝大多数现代 Linux 上都是 overlay2,下面全部按它讲。
1.2 整体结构
| 路径 | 装什么 | 能不能手删 |
|---|---|---|
overlay2/ |
镜像层与容器层的实际文件(大头) | 不能 |
image/overlay2/ |
元数据:镜像名、镜像 ID、层 ID 的对应关系 | 不能 |
containers/ |
每个容器的配置与日志 | 不能,用 docker rm |
volumes/ |
数据卷的内容 | 不能,用 docker volume rm |
buildkit/ |
构建缓存 | 可以用 docker builder prune |
一句话分工:元数据区是目录,层数据区才是书。
二、overlay2 的四个子目录

每个层在 overlay2/ 下对应一个以随机 cache-id 命名的目录,里面有四样东西:
| 子目录 / 文件 | 作用 |
|---|---|
diff/ |
★ 这一层真实的文件内容(增删改)。注意它只含本层改动,不是完整文件系统 |
link |
一个短名称,给 lower 引用,避免路径过长 |
lower |
该层依赖的下层列表 |
work/ |
OverlayFS 内部工作目录,做 copy-up 时用,不用管也不用清 |
merged 是另一个容易混淆的目录:它只在容器运行时存在 。它的内容是把只读层(lowerdir)与可写层(upperdir)联合挂载后的合并视图------你在容器里看到的 / 就是它。还没运行过的镜像没有 merged 目录,这不是镜像缺失。
📌 记住一句:
diff是「这一层改了什么」,merged是「合起来长什么样」。前者一直在,后者用完就拆。
三、四级映射:从 centos:8 找到磁盘目录

这条路要走四跳,因为四个标识互不相同:
第一跳:镜像名 → 镜像 ID
bash
[root@docker ~]# cat /var/lib/docker/image/overlay2/repositories.json
{"Repositories":{"centos":{"centos:8":"sha256:eeb6ee3f..."}}}
第二跳:镜像 ID → 层 ID 列表
image/overlay2/imagedb/content/sha256/<镜像ID>(文件名是去掉 sha256: 前缀的那串)里,RootFS.Layers 按顺序列出构成该镜像的所有层 ID。
bash
[root@docker ~]# docker image inspect centos:8 --format '{{json .RootFS.Layers}}'
第三跳:层 ID → cache-id
这是最关键的一步:overlay2 的目录名不等于层 ID ,而是一个随机 cache-id。layerdb/sha256/<层ID>/cache-id 这个文件的内容,就是该层在 overlay2/ 下的实际目录名。
第四跳:cache-id → 文件
/var/lib/docker/overlay2/<cache-id>/diff/ 就是这一层的真文件。
一条命令走完四跳:
bash
[root@docker ~]# for l in $(docker image inspect centos:8 --format '{{json .RootFS.Layers}}' | jq -r '.[]'); do
> cat /var/lib/docker/image/overlay2/layerdb/sha256/${l#sha256:}/cache-id; done
7e78d40a938c39b51b84a87cd56bf1baea6a8fbfd4c252fda16c494b0155cf32
拿到这个值,拼上 overlay2/ 就能 du -sh 看这层占多少:
bash
[root@docker ~]# du -sh /var/lib/docker/overlay2/7e78d40a938c.../diff
⚠️ 注意 :
${l#sha256:}是去掉前缀的意思。少写这一段会找不到文件,报No such file or directory------不是层坏了,是路径拼错了。
四、清空间:先摸底,再动手
绝对不要手删 /var/lib/docker/ 下的文件。 那里的内容完全由 dockerd 管理,手改会破坏元数据与层数据的一致性,轻则镜像列表错乱,重则整个 daemon 起不来。
正确的顺序是「先看、后清」:
bash
# 1) 先看各类资源各占多少
[root@docker ~]# docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 5 2 517MB 379MB (73%)
Containers 2 1 1.2kB 1.2kB (100%)
Local Volumes 0 0 0B 0B
Build Cache 0 0 0B 0B
# 2) 看一级子目录谁最大(排查时用,别拿来删)
[root@docker ~]# du -h --max-depth=1 /var/lib/docker/overlay2 | sort -rh | head
| 命令 | 清什么 | 风险 |
|---|---|---|
docker image prune |
悬空镜像(<none>) |
零风险 |
docker builder prune |
构建缓存 | 零风险,下次构建慢一点 |
docker image prune -a --filter "until=168h" |
7 天前创建且无容器引用的镜像 | 可控 |
docker system prune |
停止的容器、悬空镜像、无用网络 | 中 |
docker system prune -a --volumes |
上面全部 + 所有未使用镜像 + 数据卷 | 破坏性最强,最后手段 |
docker system df 的 RECLAIMABLE 那一列才是真正能腾出来的空间,别拿 du 的结果当账。
五、daemon.json:四个字段决定能不能用
bash
[root@docker ~]# cat /etc/docker/daemon.json
{
"registry-mirrors": ["https://abcdef123456.mirror.swr.myhuaweicloud.com"],
"insecure-registries": ["hub.zhb.cloud"],
"hosts": [
"unix:///var/run/docker.sock",
"tcp://0.0.0.0:2375"
]
}
| 字段 | 管什么 | 不配的后果 |
|---|---|---|
registry-mirrors |
拉公共镜像时的加速站,按顺序尝试 | 直连 docker.io,国内基本拉不动 |
insecure-registries |
允许用 HTTP 访问的私有仓库 | 私有 Harbor 只开 HTTP 时报 http: server gave HTTP response to HTTPS client |
hosts |
daemon 监听哪些地址 | 只监听本地 unix socket,别的机器连不上 |
log-driver / log-opts |
容器日志驱动与大小上限 | 日志无限增长,把磁盘写满 |
前两个在《Docker 架构与部署》的 3.5 里讲过用法。registry-mirrors 放加速站,这里填的是华为云 SWR 的加速地址;insecure-registries 放只开 HTTP 的自建 Harbor 域名。两个字段不能互换。
这里重点说 hosts------它是最容易配了不生效的一个。
5.1 hosts 为什么会被 ExecStart 顶掉

发行版的 docker.service 里,启动命令是写死的:
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
-H fd:// 是从命令行指定监听地址。命令行参数和 daemon.json 的 hosts 同时存在时,docker 会直接启动失败 (报 unable to configure the Docker daemon with file /etc/docker/daemon.json: the following directives are specified both as a flag and in the configuration file: hosts)。所以想让 hosts 生效,必须先把命令行里的 -H 去掉。
正解是用 systemd drop-in 覆盖,不动原文件:
bash
[root@docker ~]# systemctl edit --force docker.service
在打开的编辑器里写:
ini
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd --containerd=/run/containerd/containerd.sock
ExecStart= 那一行是空的,这是关键。 systemd 的规则是:同一指令第二次赋值视为追加,只有先赋一个空值才能把原值整体清掉。少了这行,两个 ExecStart 会拼在一起,还是冲突。
📌 另一种做法是直接编辑
/usr/lib/systemd/system/docker.service里的ExecStart,把-H加在命令行上,完整写法见《Docker 架构与部署》的 4.1。两种做法效果等价,区别只在文件归属:那份单元文件属于docker-ce包,yum update一次就被装回原样,所以长期维护的机器用本文这套 drop-in,一次实验用那种也不妨。
5.2 让配置生效
bash
[root@docker ~]# systemctl daemon-reload
[root@docker ~]# systemctl restart docker
[root@docker ~]# lsof -i :2375
dockerd 2565 root 3u IPv6 76021 0t0 TCP *:2375 (LISTEN)
[root@docker ~]# docker info | grep -A2 "Registry Mirrors"
daemon-reload 重读 unit 文件,restart 才让新参数生效。只做 restart 不做 daemon-reload,改的 override.conf 不会被读到。
⚠️ 注意 :
hosts配错(比如写成tcp://127.0.0.1:2375却期待远程能连)时,docker 不会报错,只是连不上。判据永远是lsof -i :2375里显示的是*:2375还是127.0.0.1:2375。
六、一个容易被忽略的变化:containerd image store
以上全部针对传统的 overlay2 graph driver 。从 Docker Engine 29.0 起,全新安装默认改用 containerd image store 。存储路径也随之变成 /var/lib/containerd ,与 /var/lib/docker 完全独立。
两个实际影响:
- 占更多空间:containerd store 会同时保留压缩层和解压层,同样的镜像可能比 overlay2 模式大不少;
- 路径对不上 :所有按
/var/lib/docker/overlay2写死的脚本、巡检项、清理任务都会失效。
从旧版本升级上来的机器会继续用 overlay2,除非手动启用 containerd store。所以看到 /var/lib/docker/overlay2 里有正常内容,说明还在经典模式------这也是判断环境类型最快的办法。
bash
# 看当前用的是哪种镜像存储
[root@docker ~]# docker info | grep -i "storage driver"
七、镜像拉不下来时的另一条路:proxychains-ng
镜像加速站也会挂。想让单条命令 走代理而不影响整机,用 proxychains-ng:它通过 LD_PRELOAD 拦截程序发出的连接,转发到代理。
bash
# CentOS 7 要先启用 EPEL 源
[root@docker ~]# yum install -y epel-release
[root@docker ~]# yum install -y proxychains-ng
# 在 /etc/proxychains.conf 末尾的 [ProxyList] 里加一行
[root@docker ~]# vim /etc/proxychains.conf
[ProxyList]
socks5 127.0.0.1 1080
# 或 http 代理
# http 192.168.1.10 8080
# 任何命令前加 proxychains4 就强制走代理
[root@docker ~]# proxychains4 curl ifconfig.me
[root@docker ~]# proxychains4 docker pull nginx
⚠️ 注意 :
proxychains只代理 TCP ,ICMP 不走------所以ping依然不通,别拿 ping 判断代理有没有生效。用curl或docker pull验证。
八、排错速查
| 现象 | 大概率原因 | 处理 |
|---|---|---|
docker info 里没有 Storage Driver 行 |
服务没起 | systemctl status docker |
overlay2/xxx/diff: No such file |
层 ID 前缀没去掉,路径拼错 | 用 ${l#sha256:} 去掉前缀 |
镜像列表里有 <none> 占空间 |
悬空镜像 | docker image prune |
hosts 配了但只有本地能连 |
ExecStart 里的 -H fd:// 没清 |
用 drop-in 覆盖,ExecStart= 先赋空 |
| 改完配置毫无变化 | 只 restart 没 daemon-reload,或根本没重启 | systemctl daemon-reload && systemctl restart docker |
私有仓库报 http: server gave HTTP response to HTTPS client |
仓库只开 HTTP,没进白名单 | 加进 insecure-registries |
| 磁盘莫名少了十几 GB | 容器日志没限大小 / 构建缓存堆积 | 配 log-opts,跑 docker builder prune |
📚 参考资料(官方文档)
- dockerd 配置项与 daemon.json:https://docs.docker.com/reference/cli/dockerd/
- 用 systemd 控制 dockerd 启动参数:https://docs.docker.com/engine/daemon/
- overlay2 存储驱动说明:https://docs.docker.com/engine/storage/drivers/overlay2-driver/
- 清理未使用资源(prune):https://docs.docker.com/engine/manage-resources/pruning/
- containerd image store:https://docs.docker.com/engine/storage/containerd/
附:术语英文全称速查表
| 缩写 / 名词 | 英文全称 | 中文 |
|---|---|---|
| Storage Driver | Storage Driver | 存储驱动 |
| graph driver | graph driver | 图形驱动(旧称,指同一层抽象) |
| lowerdir / upperdir | lower directory / upper directory | 只读下层 / 可写上层 |
| copy-up | copy-up | 写时复制:改下层文件时先复制到可写层 |
| union mount | union mount | 联合挂载 |
| drop-in | drop-in unit file | 覆盖片段(systemd 的局部配置) |
| LD_PRELOAD | --- | 动态链接器的预加载机制(被代理工具用来劫持连接) |
排存储问题只有一条路径:
docker info确认驱动 →repositories.json拿到镜像 ID →imagedb拿层 ID →layerdb拿 cache-id →overlay2/<cache-id>/diff看文件。四跳走通,所有「镜像在哪」的问题都有答案。而配置类问题更简单:daemon.json管拉取与监听,systemd 的ExecStart管命令行参数,两者冲突时以失败启动告终 ------先清ExecStart,再写hosts,最后daemon-reload加restart,顺序不能反。
