玩Docker这几年我发现一个很有意思的现象大多数新手第一周就能把docker run、docker compose up敲得飞起容器能跑起来甚至能直接部署一套 MySQL可一旦遇到“自己写 Dockerfile 构建镜像”或者“给现有镜像做瘦身优化”这类任务马上就卡壳。原因多半在于——镜像这个概念始终停留在“听说过”的程度。容器只是镜像的运行时表现镜像才是整个容器世界的设计和分发单元。如果你跟着这个系列一路看过来应该已经对 Docker 的基本安装和常用命令不陌生了那这一篇正好进入核心地带把镜像本身彻底吃透。我会从镜像和容器的关系讲起再用 OverlayFS 这类联合文件系统的思路解释分层机制接着通过实际 Dockerfile 演示构建与优化最后把仓库管理、镜像安全和常见故障排查都过一遍。这一篇定位是“镜像深度入门”你不需要事先理解内核底层但如果你能跟着亲手跑一遍命令收获会大得多。1. 先把镜像和容器的关系彻底捋清楚1.1 镜像不是容器容器也不是镜像我见过很多人把镜像和容器当成两个同义词随意混用用错了其实也无伤大雅但一旦要排查问题这种模糊认知就会带来麻烦。举个例子你执行docker run nginx:1.27启动了 Nginx然后进去改了/etc/nginx/nginx.conf最后把这个容器删掉你觉得 Nginx 配置的修改还在吗答案是不在了。因为文件系统的改动写在容器自己的可写层里与镜像本身没有关系。从实现层面看镜像一个静态的、只读的文件系统快照由多层构成每一层都对应镜像构建过程中的某个指令或变更。容器则是镜像的运行实例Docker 会基于镜像的文件系统额外创建一个可写层然后通过 Namespace 和 Cgroups 帮它建立隔离环境与资源限制最终通过挂载把这套只读层加可写层组合成容器内的根文件系统。这个可写层只在容器存活期间存在容器一删所有未提交的改动全部消失。在思想模型上我总是跟别人说镜像就像一本菜谱容器就是按菜谱现场做出来的那桌菜。菜谱不会因为某次做菜火候大了就变化下次照着再来一桌还是同样的味道。1.2 镜像里面到底装了什么确定“里面有什么”最直接的方式是检查镜像的历史记录和配置。我拿官方 Nginx 镜像举例docker history --no-trunc nginx:1.27你会在输出里看到很多行每一行对应的其实是 Dockerfile 里某条指令产生的层。有的是基础环境比如 Debian 系统文件有的是COPY进去的脚本和配置有的是设置ENV、ENTRYPOINT、CMD这类元数据。镜像本身不包含操作系统内核它底层直接共享宿主机内核这也是为什么容器镜像可以比整套虚拟机镜像小一个数量级。再看配置层面docker inspect nginx:1.27输出里会有完整的Config包括Env、Cmd、Entrypoint、ExposedPorts。这些内容不属于文件系统却决定了容器启动时的进程行为和监听端口。补充一个容易混淆的点ExposedPorts只是“声明”容器内会监听某个端口并不自动映射到宿主机。你真的要让宿主机访问容器还是必须显式加-p 8080:80。从内容构成理解镜像就三件事文件系统快照、容器初始配置、构建历史元数据。之前有人问为什么 Debian 基础镜像解压后可能占用三百多兆下载体积却才一百兆左右而 alpine 才几兆差距主要来自是否集成了 glibc、shell、包管理器这一类常用组件。镜像做得越大网络分发和启动速度越受影响。后面讲构建时会专门聊怎么控制这个体积。1.3 “只读模板”在运行时的真正含义如果只是把“只读模板”当一句话记住你真的运行起来还是会懵。镜像的只读属性不是靠权限控制实现的“不能改”而是文件系统层面的“不让写”。所有镜像层都是只读挂载容器内对文件的操作会走写时复制也就是把需要修改的文件从只读层复制到容器可写层再在可写层里修改。这样设计带来的好处非常直接镜像永远可以复用同一份镜像可以同时启动几十个甚至几百个容器大家共享镜像层数据不会互相污染。我经常遇到的一个场景就是同一个基于postgres:16的镜像在测试环境同时启动三个数据库容器分别给不同的项目用。因为各自的可写层完全独立就算在 A 容器里删了数据库B 容器里的数据也一丁点都不会受影响。关于这个机制不少人在“镜像与容器文件系统到底是什么关系”那里绕不过弯。我建议你手动做一个实验基于同一个镜像启动两个容器在第一个里面创建一个文件然后在第二个里看看文件是否存在。如果存在那是挂载了共享卷如果不存在说明你理解了镜像层之外的容器可写层隔离。我自己做这个实验之后很多类似问题一通百通。2. 镜像分层与写时复制整个 Docker 体系的基石2.1 分层带来的共享与复用价值镜像为什么一定要分层你想想看如果镜像就是一个大 tar 包每次构建都要整包打包、整包传输、整包解压那无论磁盘占用还是网络开销都无法忍受。分层之后多个镜像之间可以共享相同的基础层。举个例子nginx:1.27、redis:7.2、python:3.12-slim这三者如果都基于同一个 Debian 版本作为底层那么在宿主机上只需要存一份对应版本的系统基础层即可三个镜像各自只需要保存自己特有的层。从docker images看三个镜像都显示自己的完整体积但实际占用的物理磁盘空间远小于三者体积之和。这个特性在你批量部署应用时价值尤其明显几十个本地镜像里的大量公共层只存储一次宿主机磁盘压力大大降低。这里补一句在构建时非常容易踩到的坑分层共享的前提是镜像层的内容和标识一致。哪怕你的 Dockerfile 只是换了一行基础镜像的 tag比如从debian:bookworm换成debian:bookworm-slim系统层的内容不同后面所有依赖这个层继续构建的其他镜像也无法复用缓存了。2.2 写时复制容器为什么能这么轻写时复制Copy-on-Write简称 CoW是整个容器存储里的核心机制。概念讲起来很晦涩我习惯用“多联画稿”来类比镜像的每一层是一张半透明的画稿层叠在一起才是一幅完整画面的图案容器运行的时候Docker 在最上面加一张空白的透明画稿所有新增笔画都画在这张空白稿上。如果要在已画好的图案上改动某一块颜色美术师不会直接擦掉底下那层而是把那一小块区域重新画在新稿上。最终你看到的画面是全部层叠加后的结果新旧稿重合后新内容覆盖旧内容。镜像层就是那些画好且不再改动的稿纸容器可写层便是最上面那张透明稿。这么做最大的收益是启动速度和内存/磁盘效率。试想如果没有整个机制每次启动一个容器都要把镜像完整复制一份100 个容器就是 100 份完整镜像既慢又浪费。有了 CoW100 个容器共享同一套只读镜像层各自只有微小的可写层启动时几乎不需要额外复制文件秒级起容器就是这么来的。2.3 镜像层缓存构建提速的最大杠杆Docker 在构建镜像时会按 Dockerfile 的指令逐条执行每条执行成功后都生成一个层并把这个层记录到本地缓存。下次构建遇到相同的基础镜像、相同的指令和相同的上下文文件Docker 会直接复用缓存层不去执行对应步骤。很多人不知道这个机制导致明明只改了一行代码每次构建却要重新装一遍依赖白白浪费大量时间。利用缓存的核心策略是把“不怎么变化的步骤”放在 Dockerfile 前面“频繁变化的内容”放在后面。最经典的例子是这样的先COPY requirements.txt再执行RUN pip install最后才COPY整个项目源码。FROM python:3.12-slim WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY . . CMD [python, app.py]这样项目源码一改重新执行docker build时pip 安装步骤直接命中缓存构建时间从几分钟缩短到几秒。如果反过来先COPY . .再RUN pip install任何源码变化都会让 pip 重装一遍这是新手最常踩的坑之一。需要注意几个细节对于COPY和ADD缓存命中依赖的是文件和元数据权限、所有者等是否变化对于RUN如果指令执行时会访问外部网络拉取软件包即便命令文本没变外部包可能有更新缓存也可能不准确想强制跳过缓存重跑全部步骤加--no-cache参数即可但生产环境不要随意这么做缓存命中本身也是优化的一部分。3. 从零构建一个镜像Dockerfile 实操与优化3.1 基础镜像的选择思路构建镜像的第一步通常不是写指令而是决定FROM哪个基础镜像。基础镜像决定了最终镜像的体积下限、系统库兼容性和安全更新频率。我给几个常见选择做个对比基础镜像特点适用场景debian:bookworm-slim稳定、glibc、体积适中大多数跑二进制服务的场景兼容性最好ubuntu:22.04生态熟悉、资料多习惯 Ubuntu 的团队文档和现成脚本最多alpine:3.19体积极小、musl libc追求极致体积、不依赖 glibc 的静态编译程序distroless极小、无 shell安全表现好对安全要求高、调试流程完善的服务scratch空镜像几乎零体积纯静态二进制可直接运行我个人的偏好是除非对体积有极度敏感否则优先选debian:bookworm-slim这类方案。alpine 体积确实诱人但很多从网上下载的二进制、预编译 .so 动态库依赖的是 glibc放到 alpine 里一跑就报not found。有一次我为了把一个服务压到 30MB 以下选了 alpine结果一个闭源 SDK 在容器里跑不起来排查了半天才发现是 musl 和 glibc 之间的兼容性问题最后只能退回 debian-slim。这类教训一次就够。另外基础镜像的 tag 也需要注意。直接写FROM python:3.12没问题但生产环境我更建议固定到具体小版本甚至使用摘要引用比如FROM python:3.12-slimsha256:xxxx。latest标签我不建议在线上使用因为它指向的内容会变化一次构建和下一次构建之间可能默认行为就完全不同最终结果不可复现。3.2 一个规范 Dockerfile 的完整拆解我用一个 FastAPI 服务做例子这套写法在生产环境很常见# syntaxdocker/dockerfile:1 FROM python:3.12-slim AS builder WORKDIR /app ENV PIP_NO_CACHE_DIR1 \ PIP_DISABLE_PIP_VERSION_CHECK1 COPY requirements.txt . RUN pip install --prefix/install -r requirements.txt FROM python:3.12-slim RUN useradd --create-home --shell /sbin/nologin appuser WORKDIR /app COPY --frombuilder /install /usr/local COPY . . USER appuser EXPOSE 8000 CMD [uvicorn, app.main:app, --host, 0.0.0.0, --port, 8000]这里有几个关键点需要拆开讲。--prefix/install指定了 Python 依赖安装到单独目录方便二阶段里精确复制依赖文件而不是把一阶段构建时产生的操作系统缓存、临时文件都带到最终镜像里。useradd创建一个普通用户appuser并且把容器内默认用户切换成它。很多人以为容器里的 root 和宿主机 root 是一样的实际上容器内的 root 会受到 Namespace 的影响但即便如此以 root 跑业务进程仍然是一种高风险做法。一旦容器内的应用被攻破并通过漏洞逃逸root 权限会让影响范围急剧扩大。所以能不用 root 就不用 root。COPY --frombuilder是多阶段构建的关键语法它允许从上一阶段的文件系统里复制文件而不用把上一阶段整个镜像保留。最终镜像里只有运行所需的 Python 环境和应用代码构建工具链、pip 缓存全部被丢弃。EXPOSE只是元数据不是端口映射。要让宿主机能访问容器还需要docker run -p 8000:8000。CMD则定义了容器的默认启动命令如果你在docker run后面额外指定了命令这里的CMD会被覆盖。3.3 多阶段构建与镜像瘦身实战多阶段构建是“一次构建、两套环境”的典型套路。构建环境需要完整的编译器、头文件、包管理工具运行时环境只需要最后的产物。拿 Go 服务举例这是收益极其明显的场景FROM golang:1.22 AS builder WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED0 go build -ldflags-s -w -o /app/server . FROM alpine:3.19 COPY --frombuilder /app/server /usr/local/bin/server EXPOSE 8080 CMD [server]CGO_ENABLED0会编译出纯静态二进制不依赖任何动态库因此才可以放进精简的 alpine 甚至 scratch 镜像里。-ldflags-s -w是用来去掉符号表和调试信息让二进制进一步变小。单阶段构建的 Go 镜像可能轻易超过 1GB因为 golang 基础镜像本身带了完整的 Go 工具链用上面的多阶段写法最终镜像可能只有 20 多 MB。差距就是这么夸张。除了多阶段构建镜像瘦身还有几个实用手段合并 apt 安装指令并在安装完成后清理缓存文件RUN apt-get update \ apt-get install -y --no-install-recommends curl \ rm -rf /var/lib/apt/lists/*不要把临时下载的压缩包放在镜像层里用RUN里一条命令完成下载、解压、删除.dockerignore排除无用文件构建时就不会把node_modules、.git、日志文件这些脏东西打进去不要随手安装 vim、curl 这类调试工具容器调试优先考虑docker exec配合宿主命令或者临时起一个工具容器来排查。4. 镜像仓库、命名与拉取分发4.1 镜像的完整命名规则很多人写docker pull nginx时并不清楚背后到底访问了谁。Docker 镜像的完整名字一般长这样registry-host:port/namespace/repository:tag拆开看registry-host:port是镜像仓库服务的地址不写时默认走 Docker Hubnamespace一般是组织、个人用户或团队空间名repository是具体镜像仓库名tag是版本标识不写时默认latest。比如nginx:1.27等价于docker.io/library/nginx:1.27bitnami/postgresql:16里的bitnami就是命名空间postgresql是仓库名自建仓库的镜像就会长这样myregistry.example.com:5000/team/app:v1.0我在团队里强烈建议线上服务把镜像 tag 固定到具体的版本号甚至用镜像摘要digest而不是依赖latest。latest是个变化中的标签今天拉到的和三个月后拉到的可能完全不是同一个镜像这对可复现部署来说很危险。4.2 镜像的常用操作与注意事项命令作用常见注意点docker pull nginx:1.27拉取镜像网络原因失败时看具体错误码docker push myregistry/team/app:v1.0推送镜像到仓库先docker login认证私有仓库还要处理 httpsdocker tag a b给镜像打标签本质是创建引用不是复制docker rmi image删除镜像有容器使用时会报错需要先删容器docker image prune清理无引用镜像只清理悬空镜像可加-a清理全部未使用docker image inspect查看镜像详情能看到入口、环境变量、端口等元数据这里有个细节docker rmi删除镜像时如果还有容器引用它Docker 会拒绝删除并提示容器还在使用。这其实是保护机制避免你在容器还在跑的时候把依赖层删干净导致运行异常。清理一批镜像前我习惯先跑docker ps -a把还在引用旧镜像的容器确认一遍再决定是否删除。4.3 私有仓库搭建与镜像加速配置公共仓库虽然方便但很多团队并不愿意把包含业务代码、私有配置的镜像放在外面。这时候自建私有仓库就成了刚需。用官方registry:2起一个最简单的基础仓库非常快docker run -d -p 5000:5000 --name registry registry:2跑起来之后本地镜像就可以推送上去docker tag nginx:1.27 localhost:5000/team/nginx:1.27 docker push localhost:5000/team/nginx:1.27但registry:2太“素”了生产环境我一般推荐 Harbor。它基于 registry 封装提供 Web 管理界面、基于角色的访问控制RBAC、镜像复制同步、漏洞扫描等功能。团队里有多个项目、多个环境时Harbor 的“项目-仓库”组织方式和镜像复制策略真的能省掉大量手工操作。如果你不是自建仓库而是使用云服务商托管的镜像仓库我建议在daemon.json里配置镜像加速源。Docker 读取/etc/docker/daemon.json里的registry-mirrors比如这样{ registry-mirrors: [https://docker.m.daocloud.io] }配置后重启 Docker 服务即可。这类镜像加速能力在公网环境尤其在流量高峰期效果明显但是注意它只对 Docker Hub 的公共镜像拉取起作用你自建仓库或使用云厂商私有仓库的镜像还是走原来的地址。5. 镜像安全隐患藏在每一层5.1 镜像为什么是安全短板镜像的分层机制带来效率和共享但它同时埋了一个安全隐患漏洞可以藏在任意一层里。很多人在做安全评审时只检查了最上层的业务依赖却忘记了基础镜像里的系统库可能存在旧版本漏洞。而镜像一旦被构建出来这些历史层就成了“历史遗留问题”如果不定期重建漏洞会一直跟随着镜像传播出去。另一个常见的风险点是镜像里的“多余工具”。由于很多人习惯把 curl、wget、bash、vim 全装进镜像方便出了问题再进容器里排查。但对攻击者来说这些工具同样是方便的下手点容器被攻破后攻击者可以直接用 wget 下载攻击载荷用 bash 做反弹命令用 vim 改配置。所以有一个安全理念是容器内能不用 shell 就不用 shell能删调试工具就删调试工具。这也是distroless镜像近年来受关注的原因。还有就是默认 root 运行的问题。业务进程以 root 身份运行会放大逃逸攻击的后果所以一般情况下 Dockerfile 里都应该有一个显式创建用户并切换非 root 用户的过程。可以看到很多官方镜像已经开始默认非 root 工作比如官方nginx镜像就带nginx用户但很多第三方镜像仍然习惯性用 root拉下来之后需要你自己注意。5.2 用 Trivy 扫描镜像漏洞镜像安全的第一步是把已知漏洞测出来。我目前用得最多的是 Trivy来自 Aqua Security开源免费、更新频率高、单条命令就能扫。安装好后执行trivy image --severity HIGH,CRITICAL nginx:1.27它会逐层解析镜像里安装的软件包与漏洞数据库比对输出风险列表。输出里会标注漏洞编号、影响范围、严重程度和修复版本。这样你就可以评估当前镜像是否带病上线。如果刚接触我建议从固定动作开始每个构建出来的镜像在推送之前扫一遍高危和严重级别的漏洞。不要只看数量要结合“这个问题在运行环境是否真实可被利用”来判断是否阻塞发布。举个例子alpine只带 musl libc不会受 glibc 系漏洞影响扫描到的某个 glibc 漏洞在这类镜像里就压根不适用。5.3 镜像供应链安全从依赖锁到签名校验镜像供应链安全比“扫漏洞”更往前一步它关注的是整条链路基础镜像来源是否可信、依赖版本是否锁定、构建过程能否复现、镜像在传输过程中有没有被篡改。依赖锁定是很容易见效的一项。很多 Dockerfile 里只写pip install -r requirements.txt而 requirements.txt 里没锁版本几个月后重新构建时可能装到一个新版依赖出现破坏性变更或者新的漏洞。正确做法是在 requirements 里固定完整版本或者使用哈希校验。Node.js 项目里package-lock.json、Go 项目里go.sum都是同样的思路。基础镜像也建议“锁摘要”。Dockerfile 里可以这么写FROM python:3.12-slimsha256:9e8c0db5f3c882c2a0a2f78a1d6a6941b2d912b6f0e2c0e5b192976b3135c3c锁摘要后即使同名 tag 指向新版本构建时依然使用你当初验证过的那个镜像从依赖源头保证可复现。当然代价是需要你主动关注安全更新定期评估是否升级基础镜像。5.4 镜像安全实践清单这几条我基本默认写进团队规范里基础镜像只使用官方镜像或内部维护的可信镜像拉取后做一遍摘要核验Dockerfile 中FROM固定 tag 或摘要不依赖latest创建专用运行用户进程以非 root 身份启动尽量不安装调试工具和包管理器使用多阶段构建控制最终内容镜像构建完成后做漏洞扫描高危、严重阻断发布依赖和基础镜像定期更新而不是“跑起来就不管”。6. 镜像使用中的高频故障排查记录6.1 拉不到镜像 / 拉取失败镜像拉不下来的报错五花八门但排查路径基本固定。先看错误提示是网络层面还是认证层面如果是“connection refused”或超时先确认能否访问对应仓库地址再确认本地 DNS 和防火墙是否正常如果是“unauthorized”或“denied”多半是私有仓库认证问题先执行docker login如果是“manifest unknown”说明 tag 不存在或者架构不匹配比如在 ARM 机器上拉取一个仅支持 amd64 的镜像如果是磁盘空间不足docker system df一下该清理就清理。我遇到最多的是团队里有人在 Dockerfile 里写了一个不存在的 tag构建时找不到manifest但他一直以为是网络问题。排查这种问题最快的办法是先手动docker pull对应镜像如果手动能拉下来问题就在构建环节如果手动也拉不下来再考虑网络和仓库层面。6.2 构建上下文过大导致构建缓慢docker build默认把当前目录构建上下文整体打包发送给 Docker daemon。如果你的项目目录下有个几 GB 的node_modules或者.git目录每次构建都相当于先传一次这几个 GB想快也快不了。解决办法是写.dockerignore文件把不必要的目录排除掉.git node_modules target build dist *.log .vscode .idea .env这样构建时 Docker 就不会把这些目录打包进上下文。排除了.env也符合安全习惯避免机密变量被写入镜像层。还有一个相关小技巧如果构建时只是改了需求描述或文档并不涉及代码可以临时把COPY . .这一行放到 Dockerfile 后面利用层缓存跳过重新打包的过程。但要注意最终提交前还是要保持规范顺序。6.3 镜像占满磁盘空间怎么清理用一段时间 Docker 后docker system df会显示镜像、容器、卷和构建缓存的空间占用。其中构建缓存往往是最容易被忽略的。我见过开发机磁盘爆满结果发现光 Build Cache 就占了 80GB。清理策略分几档docker system df先看空间构成docker builder prune -f清理悬空的构建缓存docker image prune -a清理所有未被容器使用的镜像如果要彻底清理docker system prune -a会把所有停止的容器、未使用的镜像、未使用的网络和构建缓存一起清掉但慎用——卷数据默认不会清这一点要看清楚。这里有个常见误区即使你docker rmi显示删除成功镜像实际占用的物理空间并没有完全释放因为构建缓存或者某个容器仍持有层引用。彻底释放空间必须先把不再使用的容器删掉再清理镜像和缓存。所以我的固定顺序是先docker container prune再docker image prune -a最后docker builder prune。6.4 Docker Desktop 在 Windows 上的启动坑在 Windows 上使用 Docker Desktop 的时候最容易遇到的问题就是启动失败而且报错信息经常是“virtualization support not detected”或者“WSL 2 installation is incomplete”。这类报错的本质是Docker Desktop 在 Windows 上并不是直接跑容器而是借助一个轻量级虚拟机来运行 Linux 内核虚拟化功能或者 WSL 2 没启用就会启动失败。正常的处理路径是确认 BIOS 里开启虚拟化技术Intel VT-x 或 AMD-V确认 Windows 功能里开启了“适用于 Linux 的 Windows 子系统”和“虚拟机平台”安装 WSL2 并执行wsl --update把 Docker Desktop 设置的资源调整到合适范围特别是内存和 CPU 不要给太少否则容器启动会莫名卡死。在 macOS 过 Docker Desktop 没有这些虚拟化开关困扰但文件挂载性能会差一些。如果你在容器里跑频繁读写宿主机文件的开发任务性能慢得明显可以考虑把数据库等重 IO 服务直接用原生进程跑在宿主机上或者在容器里使用命名卷而不是 bind mount性能差距很真实。6.5 容器时区与权限问题镜像默认使用 UTC 时区日志时间与本地时间差 8 小时是经典问题。解决办法通常是在 Dockerfile 里设置时区环境变量ENV TZAsia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime echo $TZ /etc/timezone或者在运行时挂载宿主机的时区文件docker run -v /etc/localtime:/etc/localtime:ro -e TZAsia/Shanghai myimage权限问题则是另一个高频点。有时候容器内进程需要写宿主机某个目录但镜像里用了非 root 用户宿主机目录的所有者又是另一个 uid导致Permission denied。最简单的临时方法是给运行用户指定宿主机用户的 uid但这治标不治本。更规范的做法是把宿主机目录的所有者改成容器用户对应的 uid或者在镜像里将工作目录的所有权交给运行用户这样既保持非 root 又保证可写。我自己在项目里比较推荐的做法是构建镜像时就把需要写入的目录通过RUN chown交给 appuser这样运行阶段只要挂载卷权限匹配就很少出岔子。如果确实要动态配合宿主机目录再考虑在启动脚本里做一次权限调整但不要在 Dockerfile 里给业务进程一个 root 的默认入口。聊到最后分享一个我自己的真实经历。有一次线上服务扩容实例从 20 个瞬间拉到 100 个结果所有新节点都在同时拉取同一个 900MB 的应用镜像镜像仓库直接被打成瓶颈扩容整整等了十几分钟才算完成。后来我重新梳理了 Dockerfile用多阶段构建把镜像压到 180MB同时把基础依赖层单独拆出来缓存后续扩容的速度提升了好几倍。这件事给我很深的印象镜像不只是“构建出来的一个文件”它从设计、构建、存储、分发到运行的每个环节都真实影响着你的基础设施效率。如果你只把镜像当作一个普通的打包工具那大概还没有真正用透 Docker。