Docker镜像分层文件查看方法详解

0 次阅读

Docker镜像并不是一个完整、不可拆分的单体文件,而是由多个只读层(Layer)按照顺序叠加形成。理解镜像分层结构,对于排查镜像体积过大、分析Dockerfile构建过程、定位某个文件来自哪个构建步骤等问题非常有帮助。

一、Docker镜像为什么存在分层

Docker镜像采用分层存储机制,每执行一条会产生文件系统变化的Dockerfile指令,都可能形成新的镜像层。例如:

dockerfile
FROM ubuntu:22.04

RUN apt-get update && apt-get install -y nginx

COPY app /opt/app

RUN chmod +x /opt/app/start.sh

构建完成后,可以把镜像理解为多个层依次叠加:

基础镜像层
    ↓
安装软件产生的层
    ↓
复制应用文件产生的层
    ↓
修改权限产生的层

这些层通常是只读的,容器启动后,Docker会在镜像层之上增加一个可写容器层。

这种设计最大的优势是复用。例如多个镜像都基于同一个Ubuntu基础镜像,那么对应的基础层可以共享,不需要为每个镜像重复保存一份。

因此,查看Docker镜像分层文件,不仅可以了解镜像内部结构,还能帮助分析镜像大小以及构建过程。

二、使用docker history查看镜像分层

最简单的方式是使用:

Bash
docker history 镜像名

例如:

Bash
docker history nginx:latest

典型输出类似:

IMAGE          CREATED        CREATED BY                                      SIZE
a6bd71f48f68   2 weeks ago    CMD ["nginx" "-g" "daemon off;"]                0B
      2 weeks ago    STOPSIGNAL SIGQUIT                             0B
      2 weeks ago    EXPOSE map[80/tcp]                              0B
      2 weeks ago    ENTRYPOINT ["/docker-entrypoint.sh"]             0B
      2 weeks ago    COPY docker-entrypoint.sh /                     4.61kB
      2 weeks ago    RUN /bin/sh -c ...                              40MB

这里最值得关注的是几个字段:

  • IMAGE:镜像层对应的ID。

  • CREATED:该层创建时间。

  • CREATED BY:生成这一层时执行的命令。

  • SIZE:该层增加的数据大小。

  • COMMENT:构建过程中产生的备注信息。

如果重点是分析镜像为什么这么大,可以先执行:

Bash
docker history --human nginx:latest

或者:

Bash
docker history --no-trunc nginx:latest

--no-trunc会显示完整的创建命令,分析复杂Dockerfile时尤其有用。

例如:

Bash
docker history --no-trunc myapp:1.0

通过该命令可以快速建立“Dockerfile指令”和“镜像层”的对应关系。

三、查看镜像Layer ID

如果希望进一步研究具体的镜像层,可以先查看镜像详细信息:

Bash
docker image inspect nginx:latest

为了方便查看,可以结合grep

Bash
docker image inspect nginx:latest | grep -i layer

不过需要注意,Docker版本、存储驱动以及镜像格式不同,inspect输出中的层信息并不一定完全符合底层存储目录的组织方式。

因此,docker image inspect更适合查看镜像元数据,而不是直接当作“查看层文件”的工具。

四、使用docker image inspect分析镜像信息

执行:

Bash
docker image inspect myapp:1.0

可以看到比较完整的JSON信息,例如:

JSON
[
    {
        "Id": "sha256:xxxxxxxx",
        "RepoTags": [
            "myapp:1.0"
        ],
        "Size": 184729312,
        "Architecture": "amd64",
        "Os": "linux",
        "RootFS": {
            "Type": "layers",
            "Layers": [
                "sha256:aaaa...",
                "sha256:bbbb...",
                "sha256:cccc..."
            ]
        }
    }
]

其中:

JSON
"RootFS": {
    "Type": "layers"
}

说明该镜像采用分层RootFS。

而:

JSON
"Layers": [
    "sha256:aaaa...",
    "sha256:bbbb...",
    "sha256:cccc..."
]

则代表镜像包含多个内容寻址的层。

需要注意,这些SHA256摘要并不等于宿主机上某个目录名称。在实际定位文件时,还需要结合Docker使用的存储驱动进行分析。

五、使用docker save查看镜像中的Layer文件

如果目标是直接看到镜像打包后的层文件,可以使用:

Bash
docker save -o myapp.tar myapp:1.0

然后查看压缩包内容:

Bash
tar -tf myapp.tar

通常能够看到类似:

manifest.json
repositories
abcdef123456.../layer.tar
abcdef123456.../json
abcdef123456.../VERSION
123456abcdef.../layer.tar
123456abcdef.../json

其中最重要的是:

layer.tar

它就是对应镜像层中的文件系统变化。

可以将镜像保存到本地后解压:

Bash
mkdir image-files
tar -xf myapp.tar -C image-files

然后进入某个层:

Bash
cd image-files/abcdef123456...

查看:

Bash
ls -lah

如果存在:

layer.tar

就可以进一步查看该层里面有哪些文件:

Bash
tar -tf layer.tar

例如:

opt/
opt/app/
opt/app/app.dll
opt/app/config.json

这种方式非常适合需要实际查看镜像层文件内容的场景。

六、直接查看某个Layer中的文件

假设镜像保存后得到:

image/
├── abcdef/
│   └── layer.tar
├── 123456/
│   └── layer.tar
└── manifest.json

可以使用:

Bash
tar -tf image/abcdef/layer.tar

查找指定文件:

Bash
tar -tf image/abcdef/layer.tar | grep app.dll

也可以查看某个目录:

Bash
tar -tf image/abcdef/layer.tar | grep '^opt/app/'

如果只想提取一个文件,可以使用:

Bash
tar -xf image/abcdef/layer.tar opt/app/app.dll

相比直接进入Docker宿主机的存储目录,这种方式更加安全,因为它处理的是镜像导出的独立文件,而不是Docker正在使用的底层存储结构。

七、使用docker export与docker save的区别

实际排查时,经常有人把docker savedocker export混在一起使用。

两者用途并不相同。

docker save用于保存镜像

Bash
docker save -o image.tar myapp:1.0

它会保留镜像的分层结构、配置以及相关元数据。

docker export用于导出容器文件系统

Bash
docker export -o container.tar mycontainer

导出的结果更接近容器运行时看到的最终文件系统,不再保留原始镜像的Layer结构。

可以简单理解为:

docker save
    ↓
保存镜像
    ↓
保留Layer结构

docker export
    ↓
导出容器
    ↓
得到合并后的文件系统

如果你的目标是研究“镜像由哪些层组成”,应该优先使用docker historydocker save

八、查看Docker宿主机中的实际Layer目录

Linux环境下,Docker默认可能使用Overlay2存储驱动。

可以执行:

Bash
docker info

查看:

Storage Driver: overlay2

如果确认使用的是Overlay2,可以进一步查看Docker数据目录:

Bash
docker info | grep "Docker Root Dir"

常见情况下可能是:

Docker Root Dir: /var/lib/docker

Overlay2相关目录通常位于:

/var/lib/docker/overlay2/

可以查看:

Bash
sudo ls -lah /var/lib/docker/overlay2/

某些目录中会出现:

diff
link
lower
merged
work

其中:

diff

通常对应该层实际存储的文件内容。

例如:

Bash
sudo ls -lah /var/lib/docker/overlay2//diff/

可以看到该层中的目录和文件。

但是,不建议直接修改这些目录中的任何内容

Docker的存储目录属于Docker内部管理的数据结构,直接修改可能造成镜像损坏、容器异常甚至数据丢失。

九、通过Overlay2查看层之间的关系

Overlay2的一个重要特点是通过联合挂载把多个目录组合起来。

例如某个层可能存在:

diff/
lower
link
merged/
work/

其中lower通常用于记录较低层的关联关系。

查看:

Bash
sudo cat /var/lib/docker/overlay2//lower

可能得到类似:

l/ABCDEF:l/GHIJKL:l/MNOPQR

这些短名称可以进一步与Docker的Overlay2链接目录对应。

需要特别注意的是,Docker底层存储实现属于内部机制。不同Docker版本、存储驱动和配置可能存在差异,因此不应该编写依赖具体目录结构的生产脚本。

如果只是为了分析镜像,docker historydocker save以及专门的镜像分析工具通常更加合适。

十、为什么删除文件后镜像体积仍然很大

Docker镜像分层机制最容易造成误解的地方,就是“删除文件并不会真正减少之前镜像层占用的空间”。

例如Dockerfile:

dockerfile
FROM ubuntu:22.04

RUN apt-get update && apt-get install -y some-package
RUN rm -rf /var/lib/apt/lists/*

第二条RUN虽然删除了文件,但这些文件已经存在于上一层。

新的Layer只能记录:

删除某些文件

而不能把上一层已经产生的数据物理删除。

最终效果类似:

Layer 1
├── 大量缓存文件
└── ...

Layer 2
└── 删除缓存文件的记录

最终容器看到的文件可能已经不存在,但Layer 1仍然占用空间。

因此,更合理的写法是把产生临时文件和删除临时文件放在同一个RUN中:

dockerfile
RUN apt-get update 
    && apt-get install -y some-package 
    && rm -rf /var/lib/apt/lists/*

这样临时文件不会作为最终Layer的一部分永久保留下来。

十一、使用dive分析Docker镜像分层

如果经常需要分析Docker镜像,可以考虑使用专门的镜像分析工具,例如dive

它能够以交互方式展示镜像的各个Layer,并帮助分析:

  • 每层增加了哪些文件;

  • 哪些文件在后续Layer中被删除;

  • 哪些文件没有被实际使用;

  • 哪些Layer占用了较多空间;

  • Dockerfile哪些步骤导致镜像膨胀。

使用方式通常类似:

Bash
dive myapp:1.0

相比单纯执行:

Bash
docker history myapp:1.0

dive更适合深入到文件级别进行分析。

例如一个镜像可能有几十层,docker history可以告诉你某一层增加了500MB,但如果想知道这500MB究竟是什么文件,就需要进一步查看Layer内容或者使用镜像分析工具。

十二、如何定位某个文件来自哪一层

这是查看Docker镜像分层时非常实用的场景。

假设镜像中存在:

/opt/app/config.json

首先可以查看镜像历史:

Bash
docker history --no-trunc myapp:1.0

找到可能执行:

dockerfile
COPY config.json /opt/app/

的构建层。

如果无法确定,可以使用:

Bash
docker save -o myapp.tar myapp:1.0

逐个查看:

Bash
tar -tf */layer.tar | grep 'opt/app/config.json'

如果某一层出现该文件,那么它很可能就是该文件首次加入镜像的位置。

还需要注意删除操作。某个文件可能在较早的Layer中存在,后续Layer又通过OverlayFS白化文件(whiteout)机制将其隐藏。因此,分析最终文件系统时不能只看“某层是否包含该文件”,还需要考虑后续层的删除操作。

十三、查看镜像每一层大小

最简单的方式:

Bash
docker history --human myapp:1.0

例如:

IMAGE       CREATED       CREATED BY                         SIZE
abc123      1 day ago     RUN npm install                    320MB
def456      1 day ago     COPY . /app                        80MB
ghi789      1 day ago     RUN apt-get update                 120MB

从这里很容易发现:

npm install    320MB
COPY .         80MB
apt update     120MB

如果镜像总大小异常,就可以优先检查这些大Layer。

尤其需要关注以下操作:

dockerfile
RUN npm install
RUN pip install ...
RUN apt-get install ...
COPY . .
ADD ...

如果构建上下文中包含:

node_modules/
.git/
dist/
日志文件
临时文件
测试数据
大型压缩包

执行:

dockerfile
COPY . /app

时可能一次性把大量无用数据加入镜像。

此时应该合理使用.dockerignore

十四、利用.dockerignore减少无用文件进入Layer

例如Node.js项目可以配置:

node_modules
.git
.gitignore
npm-debug.log
Dockerfile
README.md
dist
coverage

Python项目可以考虑:

__pycache__
*.pyc
.git
.venv
venv
.pytest_cache
dist
build

这样:

dockerfile
COPY . /app

时就不会把这些内容全部发送给Docker构建上下文。

需要注意,.dockerignore解决的是“不要把文件发送到构建上下文并复制进镜像”的问题,而不是清理已经产生的历史Layer。

如果一个大文件已经进入某个镜像层,后续再通过:

dockerfile
RUN rm large-file

并不能消除之前Layer中的数据。

十五、多阶段构建减少最终镜像Layer内容

对于需要编译的项目,多阶段构建往往比单纯删除文件更有效。

例如:

dockerfile
FROM golang:1.24 AS builder

WORKDIR /src
COPY . .
RUN go build -o app .

FROM debian:bookworm-slim

COPY --from=builder /src/app /usr/local/bin/app

CMD ["app"]

编译器、源代码以及构建依赖全部停留在builder阶段。

最终镜像只复制运行所需的程序。

这种方式的核心不是“把旧Layer里的文件删除”,而是根本不让构建阶段产生的内容进入最终镜像

对于Java、Go、Node.js、Python等项目,多阶段构建都具有很高的实用价值。

十六、查看Docker镜像分层的推荐方法

不同目的应该采用不同方式。

需求推荐方法
查看镜像有多少层docker history
查看每层构建命令docker history --no-trunc
查看镜像元数据docker image inspect
导出完整镜像docker save
查看Layer文件docker save + tar
查看容器最终文件系统docker export
查看Overlay2实际目录/var/lib/docker/overlay2
深入分析文件变化dive
减少构建上下文.dockerignore
减少最终镜像内容多阶段构建

对于普通问题排查,建议按照下面的顺序进行:

docker history
      ↓
docker image inspect
      ↓
docker save
      ↓
分析 layer.tar
      ↓
必要时检查 Overlay2

这样既能够快速定位问题,也可以避免一开始就直接操作Docker底层存储目录。

十七、常见误区

1. 一个Dockerfile指令一定对应一个Layer

并不是所有Dockerfile指令都会产生新的文件系统Layer。例如ENVCMDLABEL等指令主要影响镜像配置,而不是简单地增加文件系统内容。

因此不能机械地认为:

10条Dockerfile指令 = 10个文件Layer

2. 删除文件就能释放镜像空间

如果文件已经存在于之前的Layer中,后续删除通常只是让最终合并文件系统看不到它,并不会消除历史层中的数据。

3. docker export可以查看镜像Layer

docker export针对的是容器,不是镜像Layer结构。

研究镜像分层应该优先考虑:

Bash
docker save

4. 可以直接修改overlay2目录

不建议这么做。

Docker存储驱动维护着自己的元数据和层关系,手工修改内部文件可能导致Docker无法正确识别镜像或容器状态。

5. 镜像层越少越好

Layer数量并不是衡量镜像质量的唯一标准。

真正需要关注的是:

  • 最终镜像体积;

  • 是否存在无用文件;

  • 构建缓存是否合理;

  • Layer是否可以复用;

  • 构建是否具有良好的缓存命中率;

  • 是否正确使用多阶段构建。

盲目减少Layer数量,有时反而会降低Docker构建缓存的利用效率。

十八、实际排查案例

假设一个应用镜像大小达到1.5GB,可以先执行:

Bash
docker history --human --no-trunc myapp:latest

假设发现:

RUN apt-get install ...       400MB
COPY . /app                   650MB
RUN npm install               300MB

那么首先检查COPY . /app对应的构建上下文。

执行:

Bash
du -sh .

并查看:

Bash
du -sh ./* | sort -h

如果发现:

node_modules    500MB
.git             80MB
dist             60MB

就应该检查.dockerignore

如果npm install产生大量构建缓存,则可以考虑:

dockerfile
RUN npm ci --omit=dev

或者将构建过程放入独立的builder阶段。

如果apt操作产生大量缓存,则应在同一个RUN中完成安装和清理:

dockerfile
RUN apt-get update 
    && apt-get install -y nginx 
    && rm -rf /var/lib/apt/lists/*

通过这种方式分析,比单纯删除最终容器中的文件更加有效。

十九、总结

Docker镜像分层文件查看可以分为三个层次。

第一层是使用:

Bash
docker history

了解镜像由哪些构建步骤组成,以及每个Layer大致占用多少空间。

第二层是使用:

Bash
docker save

导出镜像,再通过:

Bash
tar -tf layer.tar

直接查看具体Layer中的文件。

第三层则是深入Docker存储驱动,例如Overlay2目录,从底层研究Layer之间的关联关系。这种方式适合高级排障,但不建议直接修改Docker内部数据。

如果主要目的是解决镜像过大问题,建议重点关注docker history.dockerignore、多阶段构建以及构建缓存,而不是简单地通过删除文件来压缩已有Layer。掌握Docker的分层机制后,才能真正判断一个文件究竟在哪一层产生、为什么没有被删除,以及应该从Dockerfile的哪个步骤开始优化。