十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Docker diff 详解:看清容器文件变更与运维排查

Docker diff 详解:看清容器文件变更与运维排查 Docker diff 详解看清容器里到底改了什么如果你和我一样经常需要排查容器异常、分析镜像体积、或者做安全审计那你一定有过这种经历容器跑着跑着突然多了一个不明文件或者某个目录被莫名修改你心里只有一个问题——容器里到底改了什么笨办法是docker exec进去逐个ls -la对比但找半天也不知道哪些是启动时自动生成的哪些是程序运行时写进去的。这种情况docker diff就是你的第一排查工具。docker diff算得上 Docker 命令家族里最不起眼但最实用的成员之一它用来显示容器在启动之后它的文件系统相对镜像层发生了哪些增减、修改。简单说它能告诉你这个容器从创建到运行期间对文件系统的每次“动手脚”。它能帮你快速定位异常文件、清理镜像冗余、回溯容器状态变更特别适合做运维排查、开发调试和镜像瘦身的场景。这篇文章我会从原理讲到实操再到坑位排雷把docker diff从头到尾给你捋一遍。1. 先搞清楚 docker diff 在看什么1.1 容器文件的层级结构与“可写层”要理解docker diff就要先理解 Docker 镜像和容器的存储机制。Docker 镜像是由多层只读层read-only layer叠加而成的每一层代表一组文件系统的变更。当我们启动一个容器时Docker 不会修改这些只读层而是在镜像层之上添加一个可写层writable layer也叫容器层。所有对文件系统的修改都发生在这一层里。你可以把容器层想象成一张透明胶片镜像层是底层的图案。你对容器做的任何改动都等于在这张透明胶片上画画但是不会破坏底下的图案。docker diff干的事情就是把你画的那些线条精确列出来——哪些是新增的哪些是修改的哪些是删除的。这个机制决定了容器的生命周期特性容器可以启动、停止、删除但是镜像层永远保持不变。如果你想知道一个容器从镜像启动之后都改了哪些文件去看那个可写层的变更记录就一目了然。1.2 docker diff 的输出格式与状态码在没有做任何操作时全新容器刚启动时它的可写层会记录一些初始化产生的文件。运行docker diff可以看到输出每一行由两部分组成状态码和文件路径。状态码主要有三种状态编码含义说明AAdded新增的文件或目录只存在于可写层中CChanged文件或目录发生变更比如内容被修改、属性改变权限、所有权等DDeleted从镜像层中删除的文件或目录可写层中记录了删除标记比如C /etc/nginx/nginx.conf A /var/log/nginx/access.log D /etc/nginx/conf.d/default.conf这里的路径是相对于容器根目录的路径前面带不带斜杠都行。你可能会想这些路径命令输出得是不是绝对路径是的它是从/开始描述的路径但注意它输出的是容器文件系统里的路径而不是宿主机的路径。有一点需要特别说明当你修改一个文件时Docker 是把整个文件复制到可写层再在可写层里做修改所以C状态表示的是这个文件在可写层中被整体覆盖过而不是只记录差异的部分。这也是为什么docker diff对于大文件来说可能显示C但实际变更只有很小一部分。2. 什么场景下必须用 docker diff2.1 排查容器异常改动生产环境救火最常见的场景是容器运行一段时间之后服务突然报错你怀疑是某个配置文件被改动了或者某个目录下多了不该有的文件。比如 PHP 容器被挂马第一反应就是看容器里哪些文件被新增或者修改了。docker diff能直接帮你在海量路径中找到可疑项。拿一个我真实踩过的例子说吧之前我的一个 Nginx 容器出现过首页被篡改的情况。正常情况下首页文件index.html是从镜像里打包进去的容器运行后应该保持不变。结果我docker diff一看输出里有一个A /usr/share/nginx/html/backdoor.php瞬间就定位到了问题路径。不用再一层层目录翻比自己find快多了。再比如 MySQL 容器中莫名多出大量.ibd文件用docker diff可以对比容器初始状态和当前状态找出哪些是新生成的文件再结合日志判断是否业务异常写入。2.2 制作精简镜像知道哪些文件是多余的如果你想基于现有容器做一个“干净”的镜像docker diff可以帮助你判断容器在运行过程中产生的文件哪些值得保留、哪些应该清理。比如一个 Java 应用容器你跑了几天后想把它重新docker commit成镜像但你又不想把日志文件、临时文件、缓存文件都打进去。这时候你先docker diff看一下把/var/log、/tmp下那些明显是运行垃圾的路径梳理出来再在 commit 前手动删掉或者写一个清理脚本让最终镜像体积小很多。我有一个习惯每次在容器里手动改完文件并成功验证后我会先跑docker diff把这个容器的变更内容记下来再据此写 Dockerfile。也就是说docker diff在我这里是“从手工操作到自动化部署”的翻译器。你把手工操作步骤全部 grep 出来看看哪些文件改了那些文件创建了再对应写成 RUN 命令比自己凭记忆写 Dockerfile 靠谱得多。2.3 安全审计与合规检查容器内的文件变更往往意味着风险。安全扫描、合规审计时docker diff可以帮你看清运行时容器与基础镜像之间的偏差。比如某些容器被植入挖矿程序往往会在/var/spool/cron/crontabs、/tmp、/usr/lib等目录添加可疑脚本。通过定期对容器做docker diff快照可以快速比较出历史变更。一个完整的安全审计通常要结合以下手段docker diff找出可疑变更路径。docker inspect查看容器启动参数、挂载信息。docker logs回溯异常进程日志。进入容器内对这些路径进行文本分析或哈希比对。docker diff只能指出文件系统的变化但文件内容是什么、是否恶意还需要你进一步分析。不过它已经帮你划定了范围极大缩小了排查面积。3. docker diff 实操从入门到进阶3.1 基本操作怎么查看一个容器的变更首先用docker ps找到你要分析的容器名称或 ID然后执行docker diff 容器名或ID比如docker diff my_nginx输出可能像这样C /etc C /etc/nginx C /etc/nginx/nginx.conf A /run A /run/nginx.pid C /var C /var/cache C /var/cache/nginx A /var/cache/nginx/client_temp A /var/cache/nginx/fastcgi_temp ...这个列表会把从容器创建开始所有在可写层中发生的变更都展示出来。如果你初期没操作太多列表会短一些。如果容器运行了很久列表可能非常长这时候你要善用grep过滤docker diff my_nginx | grep -E ^A /etc|^C /etc这样可以只看/etc目录下新增和修改的文件。有一点需要说明docker diff只对运行中的容器有效但也适用于已停止的容器吗答案是可以。只要这个容器没有被删除即使它当前是停止状态你依然可以执行docker diff因为可写层的数据还保存在磁盘上。3.2 结合 docker inspect 看准确时间线有时候容器被反复启动过你不知道这些变更究竟发生在哪一次启动过程中。没关系把docker diff和docker inspect的信息结合起来能推断出变更的先后逻辑。docker inspect可以拿到容器的启动时间、结束时间、挂载卷、环境变量等。例如docker inspect -f {{.State.StartedAt}} my_nginx你可以记录下每次启动的时间点再对照容器内文件的时间戳判断变更发生在哪个阶段。不过这里要提醒一句docker diff是累积的它显示的是容器可写层中的所有变更并不会区分是在第几次启动时改的。所以如果你需要定位每个时间点的具体变更那应该在关键操作前后分别拍快照docker diff my_nginx /tmp/diff_before.txt # 执行某个操作 docker diff my_nginx /tmp/diff_after.txt diff /tmp/diff_before.txt /tmp/diff_after.txt通过对比前后 diff 的输出你就能看到这一段时间内新增的变更了。这是我在生产环境里最常用的方法——它比单次docker diff更加动态也更适合排查特定时段的异常行为。3.3 看 docker diff 如何反映文件创建还是内容修改你可能已经注意到docker diff输出的A、C、D分别代表新增、修改、删除。但需要注意C也可能是属性变更比如文件权限发生了变化。这在我排查权限问题时非常有用。比如有次我的 Redis 容器报错说无法写 AOF 文件。我docker diff一看发现/data目录状态为C再进去看权限原来容器启动后有人手动执行过chown redis:redis /data导致目录属主变了Redis 进程却没权限写入。这种属性变更如果不用docker diff去对初始状态单纯看当前目录权限是看不出问题的。所以我把这个场景列为必查项当你怀疑容器内文件权限被改动过时docker diff的C状态就是你判断的重要线索。4. 容器运行期间的文件变更分析4.1 启动新容器快速发现初始化生成物如果你手里有一张官方镜像想看看这个镜像启动之后运行时会初始化哪些文件很简单用这个镜像启动一个新容器不要做任何额外操作然后立刻执行docker diff。这个命令列出的所有项目就是这个镜像运行时自动生成的文件列表。比如启动一个postgres:13容器docker run -d --name tmp_pg postgres:13 docker diff tmp_pg你会看到类似这些内容A /var/lib/postgresql/data C /var/lib/postgresql C /etc/postgresql A /var/lib/postgresql/data/global ...这些信息对于你做基础设施自动化非常有价值。你可以在初始化容器后拿到第一手资料知道哪些目录是数据库自己创建的哪些文件是默认生成的然后把这些路径写进你自己定制的镜像里或者用于初始化脚本的判断逻辑。4.2 从 diff 结果追踪非法文件落地这个场景前面稍微提过我再展开细讲。很多时候容器安全事件的第一现场并非网卡流量而是文件系统突然多了不该存在的东西。比如挖矿病毒、Webshell、后门用户这类东西往往就是几个文件的事。当你知道容器 ID 后直接docker diff container_id | grep -E ^A把所有新增文件列出来然后重点关注目录内文件。如果出现可执行文件尤其是带x权限的脚本或二进制就要警惕了。下面是几个我建议重点排查的路径路径安全风险/tmp临时文件被恶意写入常被用来放置挖矿程序/var/spool/cron持久化任务被植入恶意脚本/usr/local/bin二进制被替换或新增后门工具/etc/ld.so.preload动态链接库劫持、Rootkit 痕迹/www或 web 根目录Webshell 上传在容器中执行docker diff之前建议先确认容器的 PID 命名空间是否隔离正常。如果你在宿主机上直接操作docker diff使用的是 Docker 守护进程的信息不会触发容器内进程的干扰所以相对安全。另一点提醒docker diff默认是从 Docker 守护进程读取容器的文件系统元数据性能开销并不大但在生产环境的大规模容器集群里如果每个节点上都有上百个容器每次都全量 diff 仍然会产生一定开销。我的经验是不要在高峰期做全量 diff而是针对嫌疑容器做定向 diff。4.3 用 docker diff 辅助 docker commit有时候我们确实需要把容器当前状态保存为一个新镜像比如在一台临时机器上装好环境然后 commit 下来迁移到其他机器。这时候如果直接docker commit mycontainer myimage:latest会把容器可写层里所有内容都打进去包括日志缓存、临时文件、安装包等镜像体积会膨胀得很难看。正确做法是先清理容器内的无用文件删除/tmp下的临时文件、清除 apt/yum 缓存、删除日志。再用docker diff检查剩余修改看看哪些路径仍然异常。确认无关键多余文件后再执行 commit。我举个简单例子假设容器内安装了 Python 并写了几个脚本此时 diff 输出类似于A /app A /usr/local/bin/python3.11 C /etc ...如果你看到/usr/local/lib/python3.11/site-packages下面有大量__pycache__目录那说明你在构建镜像时没有排除缓存文件。你可以在 commit 前执行docker exec mycontainer find / -type d -name __pycache__ -exec rm -rf {} 2/dev/null再 diff 确认一下然后 commit镜像体积能明显降下来。docker diff在这里的定位就像“文件系统变更清单”。没有它你可能完全不知道自己 commit 出来的镜像里有没有混进奇怪的临时文件——等到镜像被推到仓库再被人 pull 下来跑出问题那就得花更多时间排查了。5. docker diff 常见坑和易混淆点5.1 挂载卷为什么看不到这不是 bug很多人在容器里创建了几十个文件但跑docker diff一点反应都没有怎么想都觉得不对。这种情况十有八九是遇到了绑定挂载卷或数据卷。我举个例子docker run -d --name test -v /tmp/host_data:/opt/app_data nginx:alpine docker exec test touch /opt/app_data/test.txt docker diff test如果你在diff输出里找不到/opt/app_data/test.txt不要奇怪。因为/opt/app_data是挂载卷它不属于容器的可写层而是直接映射到宿主机目录。docker diff只能看到容器可写层的变更挂载卷的内容变更根本不会出现在 diff 结果里。我在刚开始用 Docker 时就被这个坑困了很久以为命令出问题了。后来才明白如果你想审计挂载卷里的文件变更不能用docker diff得直接查宿主机目录或者通过docker exec进去用find、stat等工具手动排查。这是很多新手甚至部分老手都会踩的坑我特意拿出来提醒大家。另外匿名卷anonymous volume也类似。容器删除时如果带了-v匿名卷里的内容其实还会独立保留但docker diff同样无法追踪。所以如果你需要了解挂载卷里有没有被改动就得设计别的方案了。5.2 docker diff 会不会看出所有修改哪些被排除docker diff基于 overlay2 存储驱动实现它反映的是可写层相对 нижнего 层的差异。但并不是所有文件都会出现在 diff 中有几类情况需要注意挂载卷中的文件变更上一节已说明。由 tmpfs 挂载的文件路径比如/run在部分镜像中是以 tmpfs 方式挂载的重启容器后会被清空。使用docker exec修改文件时如果文件本身存储在挂载卷内同样不会被记录。此外docker diff只能展示“存在差异”的路径而不会记录文件内容的完整变化细节。也就是说它告诉你改了哪些文件但不会告诉你怎么改的。要查看具体内容你需要另想办法比如进入容器看文件内容或者对比宿主机上的副本。还有一个容易误解的地方docker diff的路径显示的是容器内的绝对路径不是宿主机路径。如果你在宿主机上跑docker diff它输出的路径不会加上宿主机/var/lib/docker/overlay2/xxx/diff前缀所以你无法直接拿着这个路径去宿主机上查看文件。如果你需要精确定位容器可写层在宿主机上的真实位置要用docker inspect -f {{.GraphDriver.Data.UpperDir}} my_container然后到这个目录去查看对应路径的文件。这条信息在做容器逃逸、取证分析时极其重要我也是后来才琢磨明白的。5.3 diff 输出太多时如何快速过滤出高价值信息当容器运行了很久docker diff结果可能成千上百行。面对一大屏输出人很容易看花眼。我通常的做法是分几步过滤。第一步先看所有删除了什么docker diff my_container | grep ^D删除文件通常意味着可能有用户删了关键配置也有可能是恶意程序为了隐藏痕迹做清理。第二步看新增的可执行文件docker diff my_container | grep ^A | xargs -I {} docker exec my_container test -x {} echo {} executable这个方案能大概筛出新增的可执行文件。注意docker exec执行时可能因为权限不足而提示需要你结合实际情况处理。第三步看修改的配置类文件docker diff my_container | grep ^C | grep -E \.(conf|ini|xml|yaml|yml|toml|json)$配置文件被改往往是导致软件行为异常的主要原因。如果容器里文件特别多建议先把 diff 导出到文件里docker diff my_container /tmp/diff_output.txt然后在宿主机上对/tmp/diff_output.txt做各种 grep、awk、sort。这比在终端里直接刷屏高效得多。6. 进阶用法用 docker diff 构建异常检测脚本当你熟悉了docker diff的用法之后可以把它固化成一个简单的检测脚本对集群里的容器做定期巡检。脚本思路很简单获取容器列表。分别记录每个容器的docker diff结果。与上一次的运行结果对比发现新增或异常变更就告警。用 Shell 或者 Python 都能实现。这里给一个极简的 Shell 示例#!/bin/bash container$1 baseline_file/tmp/diff_baseline_${container}.txt current_file/tmp/diff_current_${container}.txt docker diff $container $current_file if [ ! -f $baseline_file ]; then cp $current_file $baseline_file echo 基线已建立 else diff $baseline_file $current_file fi这个脚本的局限是不能自动判断哪些变更属于正常业务哪些属于异常。更好的方案是在脚本里维护一个白名单路径库比如容器的日志目录、缓存目录、PID 文件路径等允许变化其他路径一旦出现新增或修改则告警。我自己在好几套生产环境里都用了类似的思路配合 cron 每天执行一次再把结果推送到监控系统确实能提前发现一些隐患。比如某个凌晨脚本发现一个数据库容器多了一个从未见过的/tmp/mysql_backup.sh后面确认是同事手动上传的备份脚本虚惊一场但至少说明脚本的感知能力是有效的。这里再强调一下docker diff不是实时的它反映的是容器启动至今的全部变更。所以你要用它做异常检测必须要有基线数据。没有基线任何 diff 输出都是“一次性快照”很难判定异常。这也是很多新人用不好docker diff的核心原因——他们总是等到异常发生才去 diff而不是在容器健康时就保存一份基线。6.1 结合 docker events 做实时监控如果你需要更实时的文件变更监控docker diff做不到。docker diff是上下文回溯工具不是事件流工具。这个时候可以借助docker eventsdocker events --filter containermy_container但它只能看到容器生命周期事件不会具体到文件级别。真正文件级别的实时监控只能进入容器内使用 inotify或通过挂载目录在宿主机上用auditd监控。docker diff的定位更像是“事后取证”它和实时监控并不冲突反而可以互补。我一般会在容器启动后立刻保存一次docker diff基线然后在出现可疑事件时用最新 diff 和基线做对比能快速定位变更。6.2 用 docker diff 写出一份容器“操作报告”还有一个巧妙的应用场景把docker diff的结果作为容器操作报告。比如你帮同事排查一个问题容器里做了很多调整最后你希望能告诉同事“我改过哪些文件”。你只需在操作前保存一份 diff 快照操作后再保存一份最后对比两份差异生成报告。这比凭记忆手写一份“我改了啥”要准确得多。我经常在帮客户做容器故障诊断时用这个套路。先跑docker diff拿基线修复问题后对比输出然后直接把 diff 结果发过去客户一目了然。这样既专业又不会漏掉任何关键路径。7. 日常使用心得体会写了这么多最后还是想分享几个我实际摸爬滚打总结出来的小经验。第一条养成容器启动即保存基线的习惯。每次创建容器跑完健康检查之后顺手执行docker diff $container /tmp/${container}_baseline.txt这个动作不影响任何业务却能让你以后排查问题时节省两个小时。别指望 Docker 自动帮你保存“初始状态”容器删了就没了。就算容器还在你没存基线就无法区分哪些改动是最初就有的、哪些是后来发生的。第二条不要迷信 docker diff 输出没有变化。前面说过挂载卷是盲区但还有更隐蔽的情况。比如容器内文件被修改后又恢复原样docker diff仍可能显示C也可能显示旧路径的变更已被覆盖。具体表现取决于 overlay 的实现。如果你需要确保某一时刻的 diff 反映真实状态最稳妥的办法是重启容器后再 diff 一次。因为重启后部分临时文件会被重新初始化可写层会被清理或重置diff 结果会瘦身。第三条在写 Dockerfile 时可以把 docker diff 当作“参考答案”。很多人在编写 Dockerfile 时不确定到底该把哪些目录做成 VOLUME哪些文件属于运行时生成。你在本地手工搭建一遍环境运行一次容器保存 diff对照 diff 结果来设计 VOLUME 和持久化策略就能避免把运行时数据误删或误缓存。我见过太多团队随便把整个/var/lib/mysql或者/etc/nginx挂出来结果要么权限错乱要么丢失默认配置。如果你先跑一次docker diff看清楚哪些属于镜像自带文件、哪些是运行初始化生成的再决定挂载层级很多问题不至于发生。docker diff就像容器的“变动账本”它不会替你做决策但把所有变更一笔一笔记得清清楚楚。你越熟悉它越能在关键时刻把它用到极致。这一篇文章讲到的用法和坑都是我一点一点在实际生产环境里磨出来的希望能帮你少走几条弯路。
返回列表