Docker diff 详解:看清容器里到底改了什么
简介
容器启动后,应用可能会创建日志、修改配置、生成 PID 文件,也可能把上传文件写进容器内部。docker diff 专门用来查看这些变化:列出容器创建以来,相对于原始镜像发生变化的文件和目录,并用 A、C、D 标记变化类型。
这条命令适合排查:
- 容器运行后新增了哪些文件
- 应用是否修改了镜像中的配置
- 日志、缓存、临时文件是否写进了容器可写层
- 删除容器前是否还有重要文件没有迁移到卷中
- docker commit 前容器里到底有哪些运行时改动
基本语法
shell
docker diff CONTAINER
完整写法:
shell
docker container diff CONTAINER
docker diff 是 docker container diff 的简写形式。CONTAINER 可以填写容器名称、短 ID 或完整 ID:
shell
docker diff nginx-test
docker diff 7d4f6b123abc
容器处于运行中或已停止状态,都可以执行 docker diff。比较基准是容器创建时使用的镜像,容器重启不会清空这些变化。
输出中的 A、C、D
text
A /app/config/local.json
C /etc/nginx/nginx.conf
D /tmp/old-cache
- A(Added):新增了文件或目录。
- C(Changed):文件或目录发生了变化,可能是内容、权限、属主或其他元数据变化。
- D(Deleted):删除了原来存在于镜像中的文件或目录。
C 不代表内容一定发生变化。例如,只修改文件权限,也可能出现 C。
第一个实战:创建、修改、删除文件
下面使用体积很小的 Alpine 镜像演示,不依赖 Nginx、PHP 等额外服务。
1. 启动测试容器
shell
docker run -d \
--name diff-demo \
alpine:3.24 \
sh -c 'sleep 3600'
这里的 alpine 是 Docker Hub 上的官方镜像,3.24 是镜像标签,不是本地自定义名称。首次执行时,如果本机没有这个镜像,Docker 会自动拉取;也可以提前执行 docker pull alpine:3.24。
sh -c 'sleep 3600' 的作用是让容器内的主进程持续运行 3600 秒,也就是 1 小时。容器的生命周期通常跟主进程绑定,sleep 结束后主进程退出,容器也会变成 exited 状态。这个演示只需要一个持续存在的容器,不需要启动真实服务。
查看初始状态:
shell
docker diff diff-demo
不同 Docker 版本、存储驱动和镜像启动方式,初始输出可能不同。有些运行时目录或设备文件可能已经显示为 C,不必把这些系统条目直接当成业务文件。
2. 创建一个文件
shell
docker exec diff-demo sh -c \
'printf "debug=true\\n" > /app-settings.conf'
docker diff diff-demo
可能看到:
text
A /app-settings.conf
这个文件是容器运行后新增的,所以标记为 A。
3. 修改镜像中已有的文件
/etc/alpine-release 是 Alpine 镜像中的已有文件。下面只修改权限,不改文件内容:
shell
docker exec diff-demo chmod 600 /etc/alpine-release
docker diff diff-demo | grep '/etc/alpine-release'
输出类似:
text
C /etc/alpine-release
这个例子说明,C 不只表示文本内容变更,权限、属主等元数据变化也可能触发 C。
4. 删除镜像中已有的文件
shell
docker exec diff-demo rm /etc/alpine-release
docker diff diff-demo | grep '/etc/alpine-release'
可能看到:
text
D /etc/alpine-release
D 表示这个路径在原始镜像中存在,但在容器当前的文件系统视图中已经被删除。
如果某个文件是在容器启动后新增,随后又被删除,最终结果不一定保留 A 或 D 记录,因为当前状态已经回到了"没有这个文件"的样子。docker diff 关注的是最终差异,不是完整的操作日志。
清理容器:
shell
docker rm -f diff-demo
删除容器会同时删除容器的可写层。没有写入卷或宿主机目录的数据,也会随之失去。
第二个实战:观察 Nginx 的运行时文件
shell
docker run -d --name diff-nginx nginx:alpine
docker diff diff-nginx
输出可能包含:
text
A /run/nginx.pid
A /var/log/nginx/access.log
A /var/log/nginx/error.log
C /var/lib/nginx/tmp
实际输出会受到镜像版本、启动脚本和 Docker 运行环境影响。常见变化包括:
- /run/nginx.pid:保存 Nginx 主进程 ID 的文件。
- /var/log/nginx/*.log:访问日志和错误日志。
- /var/lib/nginx/tmp:请求处理过程中使用的临时目录。
这类输出可以回答一个实际问题:应用运行时把数据写到了哪里?如果日志持续写进容器可写层,可写层就会不断变大;如果日志本来应该持久化或集中采集,就需要重新设计日志方案。
shell
docker rm -f diff-nginx
第三个实战:为什么挂载卷里的文件看不到
容器文件系统可以简单理解为几层叠加:
text
镜像只读层
+ 容器可写层
+ Volume / Bind Mount
= 容器内看到的文件系统
默认情况下,容器中新建的文件会进入容器可写层,docker diff 可以观察到这类变化。挂载卷和绑定挂载属于独立存储,挂载路径下的内容不属于容器可写层,不能依靠 docker diff 审计其中每个文件的变化。
使用命名卷演示
shell
docker volume create diff-volume
docker run -d \
--name diff-volume-demo \
--mount type=volume,src=diff-volume,dst=/data \
alpine:3.24 \
sh -c 'sleep 3600'
docker exec diff-volume-demo sh -c \
'printf "stored in volume\\n" > /data/result.txt'
docker diff diff-volume-demo
/data/result.txt 不会像普通容器层文件一样出现在差异清单中。原因不是文件没有变化,而是文件写入了 diff-volume,不在容器可写层内。
shell
docker exec diff-volume-demo cat /data/result.txt
docker volume inspect diff-volume
docker rm -f diff-volume-demo
docker volume rm diff-volume
绑定挂载规则相同:
shell
mkdir -p ./diff-bind
docker run -d \
--name diff-bind-demo \
--mount type=bind,src="$(pwd)/diff-bind",dst=/data \
alpine:3.24 \
sh -c 'sleep 3600'
docker exec diff-bind-demo sh -c \
'printf "stored on host\\n" > /data/result.txt'
cat ./diff-bind/result.txt
docker diff diff-bind-demo
result.txt 实际写入宿主机的 ./diff-bind 目录。检查绑定挂载的变化时,直接检查宿主机目录;检查卷的变化时,在容器内查看挂载目录或使用卷自身的管理方式。
过滤输出
docker diff 本身没有按路径、状态或文件类型过滤的参数。输出较多时,可以交给 grep、awk 等命令处理。
shell
# 只查看新增内容
docker diff CONTAINER | grep '^A '
# 只查看修改内容
docker diff CONTAINER | grep '^C '
# 只查看删除内容
docker diff CONTAINER | grep '^D '
# 只查看日志路径
docker diff CONTAINER | grep '/var/log'
# 只查看应用目录
docker diff CONTAINER | grep '^. /app/'
这里的 ^. 表示匹配开头的状态字母,后面跟一个空格。
配合其他命令排查磁盘占用
docker diff 只能列出路径,不能告诉文件占用了多少空间,也不会显示文件具体改了哪几行。排查容器磁盘变大时,可以配合以下命令:
shell
# 先看容器可写层大小
docker ps --size
docker inspect --size CONTAINER
# 再看哪些路径发生了变化
docker diff CONTAINER
# 进入容器查看目录占用
docker exec -it CONTAINER sh
du -sh /var/log/* 2>/dev/null
du -sh /var/lib/nginx/* 2>/dev/null
exit
排查结果可以大致分成几类:
- docker ps --size 很大,docker diff 中日志和临时文件很多:应用把运行时数据写入了容器可写层。
- docker diff 变化不多,但业务数据很多:数据可能写进了 Volume 或 Bind Mount。
- 镜像占用空间很大,但容器可写层很小:问题更可能出在镜像层或构建缓存,docker diff 无法替代镜像分析工具。
和 docker commit 配合使用
shell
docker diff CONTAINER
docker commit CONTAINER demo-image:debug
docker history demo-image:debug
docker diff 适合在提交前确认变化范围,但不建议把长期维护的镜像构建流程依赖在手工修改容器和 docker commit 上。可复现的做法是把安装步骤、配置和文件复制写进 Dockerfile,再通过 docker build 生成镜像。
提交容器时,挂载卷中的数据不会被包含到新镜像里。因此,docker diff 看不到的卷数据,也不会因为 docker commit 自动进入镜像。
和其他 Docker 命令的区别
| 命令 | 主要用途 | 关注对象 |
|---|---|---|
| docker diff | 查看容器创建以来的文件系统变化 | 容器可写层 |
| docker inspect | 查看容器配置、挂载、网络和状态 | 容器元数据 |
| docker inspect --size | 查看大小信息 | 可写层大小等 |
| docker history | 查看镜像构建历史 | 镜像层 |
| docker ps --size | 查看容器大小 | 容器可写层和虚拟大小 |
| docker exec | 在容器中执行命令 | 容器内部 |
确认某个路径是否为挂载目录:
shell
docker inspect -f '{{json .Mounts}}' CONTAINER
查看文件的具体内容差异时,把文件复制到宿主机,再配合普通的 diff:
shell
docker cp CONTAINER:/etc/app.conf ./app.conf.container
diff -u ./app.conf.image ./app.conf.container
docker diff 负责回答"哪些路径变了",普通 diff 才负责回答"文本具体改了什么"。
常见误区
误区一:把 docker diff 当成操作日志
docker diff 展示的是当前状态与镜像之间的差异,不会记录每一次写入、重命名或删除操作的时间顺序。需要知道修改者和修改时间时,应使用应用日志、审计系统或文件监控工具。
误区二:认为重启容器会清空差异
重启只会重新启动同一个容器,容器可写层仍然保留。只有删除容器并重新创建,原来的可写层才会消失。
误区三:认为 C /var/log 就代表只有目录本身变了
目录条目可能因为目录元数据变化、子项增删或运行时行为出现在结果中。需要继续查看目录下的文件,或者进入容器检查内容和时间。
误区四:用容器可写层保存数据库和上传文件
容器可写层适合临时数据,不适合数据库文件、用户上传文件和大量日志。容器删除后,这些数据可能一起丢失;大量写入还会让可写层膨胀。
数据库、上传目录和需要跨容器共享的数据,应使用 Volume、Bind Mount 或外部存储。临时缓存则可以根据生命周期选择容器层或 tmpfs。
误区五:用 docker diff 对比两个镜像
docker diff 的对象必须是容器,不能直接填写两个镜像名。镜像层分析可以使用 docker history,需要逐层查看文件变化时可以使用专门的镜像分析工具。
一套实用排查流程
shell
# 1. 找出可写层较大的容器
docker ps --size
# 2. 查看容器有哪些路径变化
docker diff CONTAINER
# 3. 重点筛选日志、缓存、临时目录
docker diff CONTAINER | grep -E '^. /(var/log|tmp|var/tmp|app)'
# 4. 查看挂载配置,排除 Volume 和 Bind Mount
docker inspect -f '{{json .Mounts}}' CONTAINER
# 5. 进入容器确认目录实际占用
docker exec -it CONTAINER sh
如果变化集中在 /var/log、/tmp、/var/tmp、应用上传目录,重点检查日志轮转、临时文件清理和挂载配置。如果变化来自挂载路径,转而检查卷或宿主机目录,而不是继续依赖 docker diff。
总结
docker diff CONTAINER 可以记成一句话:
查看容器相对于创建时镜像,当前可写层新增、修改和删除了哪些文件。
三个边界必须记住:
- 比较的是最终状态,不是完整操作记录。
- 主要关注容器可写层,Volume 和 Bind Mount 中的内容需要单独检查。
- 只列出路径和变化类型,不提供文本级内容差异。
掌握 docker diff 后,再配合 docker ps --size、docker inspect、docker exec 和普通 diff,就能从"容器变大了"进一步定位到"到底是哪个目录、哪类文件导致的"。