概述
自己写的 Spring Boot 服务,DockerHub 上不会有人替你构建镜像,必须自己来。而要自己构建,先得搞清楚一个镜像内部到底是怎么堆起来的------这一篇只讲结构,不讲语法。
纲要
- 从"镜像是什么"这句话里拆出组成与顺序
- 为什么必须是分层:MySQL 5.7 升级到 5.8 的对比
- 镜像的两端:BaseImage 基础镜像层 与 Entrypoint 入口层
- 逐行拆解真实 Dockerfile:一条指令一层,四层怎么叠出来
- 只读分层与写时复制:容器最上面那层可写层从哪来
- 层复用的两个收益:多镜像共享 base 省磁盘、构建缓存加速
- 动手验证:
docker history看分层,docker inspect看RootFS.Layers - 实战坑:RUN 合并、
<missing>的含义、改前一行废掉后面全部缓存 - 与「Dockerfile 语法」一篇的边界
镜像的组成:回到那句话本身
镜像是把应用程序及其需要的系统函数库、环境、配置、依赖打包到一起形成的文件。
这句话听了很多遍,但镜像的结构 其实就藏在里面。"系统函数库、环境、配置、依赖、应用"是它的组成;而这些组成之间不是平铺的,是有依赖顺序的:
- 没有最底层的系统函数库和文件系统,环境变量无处可配
- 没有环境变量,依赖装不对路径
- 依赖没装好,应用跑不起来
- 应用装好了不给入口,别人不知道怎么启动
所以镜像不是把一堆文件揉成一团,而是按依赖顺序分层构建。下面这段树就是构建一个 Java 镜像时真正放在一个目录里的东西:
tree
docker-demo/
├── Dockerfile # 构建描述文件(9 条指令)
├── docker-demo.jar # Spring Boot 可执行 jar,约 25 MB
└── jdk8.tar.gz # 离线 JDK 安装包,约 177 MB
docker build 时 Docker 会读这个目录,按 Dockerfile 一路往上叠层。
为什么必须分层:一个 5.7 到 5.8 的例子
如果镜像真的是"一疙瘩",那你构建完 MySQL 5.7 镜像后,想出一个 5.8 版本,只能从最底层的文件系统开始,重装环境、重配依赖、重装应用,全程重来一遍。原来那坨东西你没法拆,也就没法复用。
分层之后,情况完全不同。构建 5.7 时就有意把每一步的产物单独放一层:
#mermaid-svg-NNTj7vigqW9Wuj3B{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-NNTj7vigqW9Wuj3B .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-NNTj7vigqW9Wuj3B .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-NNTj7vigqW9Wuj3B .error-icon{fill:#552222;}#mermaid-svg-NNTj7vigqW9Wuj3B .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-NNTj7vigqW9Wuj3B .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-NNTj7vigqW9Wuj3B .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-NNTj7vigqW9Wuj3B .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-NNTj7vigqW9Wuj3B .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-NNTj7vigqW9Wuj3B .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-NNTj7vigqW9Wuj3B .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-NNTj7vigqW9Wuj3B .marker{fill:#333333;stroke:#333333;}#mermaid-svg-NNTj7vigqW9Wuj3B .marker.cross{stroke:#333333;}#mermaid-svg-NNTj7vigqW9Wuj3B svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-NNTj7vigqW9Wuj3B p{margin:0;}#mermaid-svg-NNTj7vigqW9Wuj3B .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-NNTj7vigqW9Wuj3B .cluster-label text{fill:#333;}#mermaid-svg-NNTj7vigqW9Wuj3B .cluster-label span{color:#333;}#mermaid-svg-NNTj7vigqW9Wuj3B .cluster-label span p{background-color:transparent;}#mermaid-svg-NNTj7vigqW9Wuj3B .label text,#mermaid-svg-NNTj7vigqW9Wuj3B span{fill:#333;color:#333;}#mermaid-svg-NNTj7vigqW9Wuj3B .node rect,#mermaid-svg-NNTj7vigqW9Wuj3B .node circle,#mermaid-svg-NNTj7vigqW9Wuj3B .node ellipse,#mermaid-svg-NNTj7vigqW9Wuj3B .node polygon,#mermaid-svg-NNTj7vigqW9Wuj3B .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-NNTj7vigqW9Wuj3B .rough-node .label text,#mermaid-svg-NNTj7vigqW9Wuj3B .node .label text,#mermaid-svg-NNTj7vigqW9Wuj3B .image-shape .label,#mermaid-svg-NNTj7vigqW9Wuj3B .icon-shape .label{text-anchor:middle;}#mermaid-svg-NNTj7vigqW9Wuj3B .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-NNTj7vigqW9Wuj3B .rough-node .label,#mermaid-svg-NNTj7vigqW9Wuj3B .node .label,#mermaid-svg-NNTj7vigqW9Wuj3B .image-shape .label,#mermaid-svg-NNTj7vigqW9Wuj3B .icon-shape .label{text-align:center;}#mermaid-svg-NNTj7vigqW9Wuj3B .node.clickable{cursor:pointer;}#mermaid-svg-NNTj7vigqW9Wuj3B .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-NNTj7vigqW9Wuj3B .arrowheadPath{fill:#333333;}#mermaid-svg-NNTj7vigqW9Wuj3B .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-NNTj7vigqW9Wuj3B .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-NNTj7vigqW9Wuj3B .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NNTj7vigqW9Wuj3B .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-NNTj7vigqW9Wuj3B .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NNTj7vigqW9Wuj3B .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-NNTj7vigqW9Wuj3B .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-NNTj7vigqW9Wuj3B .cluster text{fill:#333;}#mermaid-svg-NNTj7vigqW9Wuj3B .cluster span{color:#333;}#mermaid-svg-NNTj7vigqW9Wuj3B div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-NNTj7vigqW9Wuj3B .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-NNTj7vigqW9Wuj3B rect.text{fill:none;stroke-width:0;}#mermaid-svg-NNTj7vigqW9Wuj3B .icon-shape,#mermaid-svg-NNTj7vigqW9Wuj3B .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NNTj7vigqW9Wuj3B .icon-shape p,#mermaid-svg-NNTj7vigqW9Wuj3B .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-NNTj7vigqW9Wuj3B .icon-shape .label rect,#mermaid-svg-NNTj7vigqW9Wuj3B .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NNTj7vigqW9Wuj3B .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-NNTj7vigqW9Wuj3B .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-NNTj7vigqW9Wuj3B :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ① 基础文件系统 layer
② 环境变量 / 运行环境 layer
③ 依赖安装 layer
④ MySQL 5.7 安装 layer
⑤ 配置与入口 layer
要出 5.8,只需把 ④⑤ 两层砍掉,在 ①②③ 之上重新叠。前面三层的构建时间、磁盘占用直接省下来。
这就是分层的第一层动机:复用。你构建自定义镜像时,也应该从基础开始逐层构建,别一把梭。
镜像的两端:BaseImage 与 Entrypoint
一个常见镜像里,两层是几乎必然存在的:
| 层 | 名称 | 作用 | 是否必有 |
|---|---|---|---|
| 最底层 | BaseImage 基础镜像层 | 镜像依赖的最基础东西:系统函数库、文件系统。像大楼地基 | 必有 |
| 最顶层 | Entrypoint 入口层 | 应用启动的命令脚本,告诉别人怎么把镜像跑起来 | 除非这个镜像压根不打算被启动 |
| 中间若干层 | 构建产生的 layer | 环境变量、依赖安装、应用安装、配置文件......取决于构建时做了什么操作 | 不确定,做一次操作就多一层 |
中间部分为什么"不确定"?因为别人构建镜像时中间做了哪些动作,你从外面看不出来。但有一条铁律:每执行一次操作,就产生新的一层。这个"层"就是 Layer。
逐行拆解真实 Dockerfile
讲义里给的 Ubuntu 方案,是最能说明"指令 = 层"的素材。把它整段贴出来:
dockerfile
# 指定基础镜像
FROM ubuntu:16.04
# 配置环境变量,JDK的安装目录
ENV JAVA_DIR=/usr/local
# 拷贝jdk和java项目的包
COPY ./jdk8.tar.gz $JAVA_DIR/
COPY ./docker-demo.jar /tmp/app.jar
# 安装JDK
RUN cd $JAVA_DIR \
&& tar -xf ./jdk8.tar.gz \
&& mv ./jdk1.8.0_144 ./java8
# 配置环境变量
ENV JAVA_HOME=$JAVA_DIR/java8
ENV PATH=$PATH:$JAVA_HOME/bin
# 暴露端口
EXPOSE 8090
# 入口,java项目的启动命令
ENTRYPOINT java -jar /tmp/app.jar
从结构上看,这 9 条指令对应四段职责,自下而上叠成这样:
#mermaid-svg-ceFcHVwAODvTVicu{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ceFcHVwAODvTVicu .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ceFcHVwAODvTVicu .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ceFcHVwAODvTVicu .error-icon{fill:#552222;}#mermaid-svg-ceFcHVwAODvTVicu .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ceFcHVwAODvTVicu .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ceFcHVwAODvTVicu .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ceFcHVwAODvTVicu .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ceFcHVwAODvTVicu .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ceFcHVwAODvTVicu .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ceFcHVwAODvTVicu .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ceFcHVwAODvTVicu .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ceFcHVwAODvTVicu .marker.cross{stroke:#333333;}#mermaid-svg-ceFcHVwAODvTVicu svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ceFcHVwAODvTVicu p{margin:0;}#mermaid-svg-ceFcHVwAODvTVicu .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ceFcHVwAODvTVicu .cluster-label text{fill:#333;}#mermaid-svg-ceFcHVwAODvTVicu .cluster-label span{color:#333;}#mermaid-svg-ceFcHVwAODvTVicu .cluster-label span p{background-color:transparent;}#mermaid-svg-ceFcHVwAODvTVicu .label text,#mermaid-svg-ceFcHVwAODvTVicu span{fill:#333;color:#333;}#mermaid-svg-ceFcHVwAODvTVicu .node rect,#mermaid-svg-ceFcHVwAODvTVicu .node circle,#mermaid-svg-ceFcHVwAODvTVicu .node ellipse,#mermaid-svg-ceFcHVwAODvTVicu .node polygon,#mermaid-svg-ceFcHVwAODvTVicu .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ceFcHVwAODvTVicu .rough-node .label text,#mermaid-svg-ceFcHVwAODvTVicu .node .label text,#mermaid-svg-ceFcHVwAODvTVicu .image-shape .label,#mermaid-svg-ceFcHVwAODvTVicu .icon-shape .label{text-anchor:middle;}#mermaid-svg-ceFcHVwAODvTVicu .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ceFcHVwAODvTVicu .rough-node .label,#mermaid-svg-ceFcHVwAODvTVicu .node .label,#mermaid-svg-ceFcHVwAODvTVicu .image-shape .label,#mermaid-svg-ceFcHVwAODvTVicu .icon-shape .label{text-align:center;}#mermaid-svg-ceFcHVwAODvTVicu .node.clickable{cursor:pointer;}#mermaid-svg-ceFcHVwAODvTVicu .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ceFcHVwAODvTVicu .arrowheadPath{fill:#333333;}#mermaid-svg-ceFcHVwAODvTVicu .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ceFcHVwAODvTVicu .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ceFcHVwAODvTVicu .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ceFcHVwAODvTVicu .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ceFcHVwAODvTVicu .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ceFcHVwAODvTVicu .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ceFcHVwAODvTVicu .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ceFcHVwAODvTVicu .cluster text{fill:#333;}#mermaid-svg-ceFcHVwAODvTVicu .cluster span{color:#333;}#mermaid-svg-ceFcHVwAODvTVicu div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ceFcHVwAODvTVicu .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ceFcHVwAODvTVicu rect.text{fill:none;stroke-width:0;}#mermaid-svg-ceFcHVwAODvTVicu .icon-shape,#mermaid-svg-ceFcHVwAODvTVicu .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ceFcHVwAODvTVicu .icon-shape p,#mermaid-svg-ceFcHVwAODvTVicu .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ceFcHVwAODvTVicu .icon-shape .label rect,#mermaid-svg-ceFcHVwAODvTVicu .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ceFcHVwAODvTVicu .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ceFcHVwAODvTVicu .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ceFcHVwAODvTVicu :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Layer 1 · BaseImage
FROM ubuntu:16.04
系统函数库 + 文件系统
Layer 2 · 环境准备
ENV JAVA_DIR + COPY 两个包
jdk8.tar.gz / docker-demo.jar
Layer 3 · 安装运行时
RUN 解压 JDK 并重命名 java8
ENV JAVA_HOME / PATH
Layer 4 · 应用入口
EXPOSE 8090 + ENTRYPOINT
java -jar /tmp/app.jar
容器可写层
(运行时才有,不属于镜像)
逐条对照,看得更清楚:
| Dockerfile 指令 | 产生的一层干了什么 | 为什么要单独一层 |
|---|---|---|
FROM ubuntu:16.04 |
拉来 Ubuntu 16.04 的只读文件系统作为地基 | 换个 base 就是另一个镜像,必须独立 |
ENV JAVA_DIR=/usr/local |
把 JDK 安装目录写成环境变量 | 后续所有指令都引用它,改一处不用改全文 |
COPY ./jdk8.tar.gz $JAVA_DIR/ |
把 177 MB 的 JDK 包拷进镜像 | 大文件独立成层,内容不变时永久命中缓存 |
COPY ./docker-demo.jar /tmp/app.jar |
把业务 jar 拷进镜像 | 业务代码改动最频繁,让它尽量靠上,别拖累下层 |
RUN cd $JAVA_DIR && tar -xf ... && mv ... |
解压 JDK,重命名为 java8 |
安装动作产生文件变更,天然是一层 |
ENV JAVA_HOME=$JAVA_DIR/java8 |
指向 JDK 根目录 | 运行时依赖的环境变量 |
ENV PATH=$PATH:$JAVA_HOME/bin |
把 java 命令加进 PATH |
同上,入口脚本才能直接调 java |
EXPOSE 8090 |
声明容器监听 8090 | 只是元数据,但同样记一层 |
ENTRYPOINT java -jar /tmp/app.jar |
定义启动命令行 | 镜像的入口,别人 docker run 时就靠它 |
注意 RUN 里用了 && 把三条命令连成一条------cd、解压、重命名如果拆成三个 RUN,就会多出两层。这一点后面"实战坑"里还会细说。
只读分层与写时复制
镜像的每一层都是只读的,谁都不能改。那容器运行时产生的日志、临时文件、数据库数据写哪儿去?
Docker 的做法是:启动容器时,在镜像最上面再加一个可写层。容器读文件时从最上层往下找,找到就用;写文件时,如果目标在下面的只读层里,就把那个文件先复制到可写层再改,下层保持原样不动。这就是写时复制(Copy-on-Write)。
#mermaid-svg-WO5WBsjAatAK4Ot7{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-WO5WBsjAatAK4Ot7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-WO5WBsjAatAK4Ot7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-WO5WBsjAatAK4Ot7 .error-icon{fill:#552222;}#mermaid-svg-WO5WBsjAatAK4Ot7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-WO5WBsjAatAK4Ot7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-WO5WBsjAatAK4Ot7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-WO5WBsjAatAK4Ot7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-WO5WBsjAatAK4Ot7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-WO5WBsjAatAK4Ot7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-WO5WBsjAatAK4Ot7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-WO5WBsjAatAK4Ot7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-WO5WBsjAatAK4Ot7 .marker.cross{stroke:#333333;}#mermaid-svg-WO5WBsjAatAK4Ot7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-WO5WBsjAatAK4Ot7 p{margin:0;}#mermaid-svg-WO5WBsjAatAK4Ot7 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-WO5WBsjAatAK4Ot7 .cluster-label text{fill:#333;}#mermaid-svg-WO5WBsjAatAK4Ot7 .cluster-label span{color:#333;}#mermaid-svg-WO5WBsjAatAK4Ot7 .cluster-label span p{background-color:transparent;}#mermaid-svg-WO5WBsjAatAK4Ot7 .label text,#mermaid-svg-WO5WBsjAatAK4Ot7 span{fill:#333;color:#333;}#mermaid-svg-WO5WBsjAatAK4Ot7 .node rect,#mermaid-svg-WO5WBsjAatAK4Ot7 .node circle,#mermaid-svg-WO5WBsjAatAK4Ot7 .node ellipse,#mermaid-svg-WO5WBsjAatAK4Ot7 .node polygon,#mermaid-svg-WO5WBsjAatAK4Ot7 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-WO5WBsjAatAK4Ot7 .rough-node .label text,#mermaid-svg-WO5WBsjAatAK4Ot7 .node .label text,#mermaid-svg-WO5WBsjAatAK4Ot7 .image-shape .label,#mermaid-svg-WO5WBsjAatAK4Ot7 .icon-shape .label{text-anchor:middle;}#mermaid-svg-WO5WBsjAatAK4Ot7 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-WO5WBsjAatAK4Ot7 .rough-node .label,#mermaid-svg-WO5WBsjAatAK4Ot7 .node .label,#mermaid-svg-WO5WBsjAatAK4Ot7 .image-shape .label,#mermaid-svg-WO5WBsjAatAK4Ot7 .icon-shape .label{text-align:center;}#mermaid-svg-WO5WBsjAatAK4Ot7 .node.clickable{cursor:pointer;}#mermaid-svg-WO5WBsjAatAK4Ot7 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-WO5WBsjAatAK4Ot7 .arrowheadPath{fill:#333333;}#mermaid-svg-WO5WBsjAatAK4Ot7 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-WO5WBsjAatAK4Ot7 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-WO5WBsjAatAK4Ot7 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WO5WBsjAatAK4Ot7 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-WO5WBsjAatAK4Ot7 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WO5WBsjAatAK4Ot7 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-WO5WBsjAatAK4Ot7 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-WO5WBsjAatAK4Ot7 .cluster text{fill:#333;}#mermaid-svg-WO5WBsjAatAK4Ot7 .cluster span{color:#333;}#mermaid-svg-WO5WBsjAatAK4Ot7 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-WO5WBsjAatAK4Ot7 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-WO5WBsjAatAK4Ot7 rect.text{fill:none;stroke-width:0;}#mermaid-svg-WO5WBsjAatAK4Ot7 .icon-shape,#mermaid-svg-WO5WBsjAatAK4Ot7 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WO5WBsjAatAK4Ot7 .icon-shape p,#mermaid-svg-WO5WBsjAatAK4Ot7 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-WO5WBsjAatAK4Ot7 .icon-shape .label rect,#mermaid-svg-WO5WBsjAatAK4Ot7 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WO5WBsjAatAK4Ot7 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-WO5WBsjAatAK4Ot7 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-WO5WBsjAatAK4Ot7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 可写层没有
下层只读
本就在可写层
读 /tmp/app.jar
从最上层往下找
Layer 4 只读
Layer 3 只读
Layer 2 只读
Layer 1 ubuntu 只读
命中,返回文件
写文件
目标层可写?
复制到可写层再修改
下层原文件不动
直接修改
这个设计带来两个直接后果:
- 同一个镜像可以同时跑 N 个容器,大家共用下面所有只读层,只有最上面各自一个薄薄的可写层。省的是磁盘,也是内存里共享的文件页。
- 容器删掉,只删它自己的可写层,镜像本身毫发无损。所以"容器不能持久化数据"这个坑,得靠数据卷挂载来解决。
层复用的两个收益
分层的价值不止是"好看",实测下来主要吃两头红利。
一、多个镜像共享 base 层,省磁盘。 你构建的 docker-demo:1.0 和官方 ubuntu:16.04 共用同一批底层 layer,Docker 在本地只存一份:
| 场景 | 磁盘占用 | 说明 |
|---|---|---|
| 不分层,每个镜像打包成独立文件 | 各存各的,177 MB 的 JDK 在每个镜像里各一份 | 拉 5 个镜像就多 5 份 |
| 分层,相同 layer 共享 | base 层与未变化的中间层只存一份,差异层单独存 | docker images 里看到的 SIZE 是叠加值,实际占用更小 |
二、构建缓存加速,改一行只重建它之后的层。 构建时 Docker 会逐层比对,某层的内容没变就直接复用缓存:
| 你改了什么 | 受影响的层 | 重建耗时 |
|---|---|---|
| 只改业务 jar 里的一个类 | 只有 COPY ./docker-demo.jar 之后的两层 |
秒级,前面的 JDK 解压全部命中缓存 |
改了 ENV JAVA_DIR 的值 |
从该 ENV 起的全部后续层 |
解压 JDK 的 RUN 会重跑,几十秒 |
换了基础镜像 FROM |
全部 | 从零重建 |
这解释了一个反直觉的实践:把变化频率高的东西尽量放在 Dockerfile 靠后的位置。业务 jar 天天变,JDK 几乎不变,就别让业务 jar 的 COPY 排在 JDK 解压前面。
动手验证:把分层看出来
光看图不放心,跑两条命令自己看。
用 docker build 构建完后,先看分层历史:
bash
docker build -t docker-demo:1.0 .
docker history docker-demo:1.0
输出形如(截取关键列):
text
IMAGE CREATED CREATED BY SIZE
a1b2c3d4e5f6 2 minutes ago /bin/sh -c #(nop) ENTRYPOINT ["java" "-jar"... 0B
9f8e7d6c5b4a 2 minutes ago /bin/sh -c #(nop) EXPOSE 8090 0B
7c6b5a4d3e2f 2 minutes ago /bin/sh -c #(nop) ENV PATH=/usr/local/... 0B
5d4c3b2a1908 2 minutes ago /bin/sh -c #(nop) ENV JAVA_HOME=/usr/... 0B
3b2a1908f7e6 2 minutes ago /bin/sh -c cd /usr/local && tar -xf ./jdk8... 175MB
1a0f9e8d7c6b 2 minutes ago /bin/sh -c #(nop) COPY file:... in /tmp/app.jar 25.4MB
f0e9d8c7b6a5 2 minutes ago /bin/sh -c #(nop) COPY file:... in /usr/local/ 177MB
e8d7c6b5a4f3 2 minutes ago /bin/sh -c #(nop) ENV JAVA_DIR=/usr/local 0B
c7b6a5f4e3d2 2 weeks ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B
<missing> 2 weeks ago /bin/sh -c mkdir -p /run/systemd && echo 'do... 7B
三处细节值得盯:
#(nop)表示这条指令不产生文件变化(比如ENV、EXPOSE、ENTRYPOINT),SIZE 是0B,但它仍然是一层。- JDK 那层显示的是解压后的 175 MB,而不是压缩包的 177 MB,说明它记录的是该层的净变化量。
<missing>出现的地方,是 ubuntu 官方镜像构建时留下的中间层,本地没有那一层的元数据(常见于早年非 BuildKit 构建的镜像),只能看到大小,点不进详情。这不是报错。
再看镜像的 RootFS,能直接读到层 ID 列表:
bash
docker inspect docker-demo:1.0
输出是一大坨 JSON,只看 RootFS 部分:
json
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:2a1b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f809",
"sha256:3b2c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a",
"sha256:4c3d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2b",
"sha256:5d4e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c"
]
}
Layers 数组的长度就是层数,顺序自下而上 :第一个元素是最底层的 base,最后一个离容器可写层最近。两个镜像如果 Layers 前缀相同,说明它们共享了这些底层。想只取这一段,可以:
bash
docker inspect -f '{{.RootFS.Layers}}' docker-demo:1.0
实战坑
坑一:层数堆太多,体积膨胀。 每层都会带来额外的元数据开销,更麻烦的是------在某一层删掉的文件,如果是在更下层加进来的,删除只是记录一条"删除标记",文件本体依然存在下层里,体积根本降不下来。典型写法:
dockerfile
# 反面教材:三个 RUN,三层,且临时文件只被"标记删除"
RUN apt-get update -y
RUN apt-get install -y vim net-tools
RUN apt-get clean
正确做法是合并成一条 RUN,让安装和清理发生在同一层:
dockerfile
# 推荐:一条 RUN 搞定,临时文件在同一层的末尾就没了
RUN apt-get update -y \
&& apt-get install -y --no-install-recommends vim net-tools \
&& rm -rf /var/lib/apt/lists/*
坑二:改 Dockerfile 前面某一行,后面全部缓存失效。 比如你把 ENV JAVA_DIR=/usr/local 改成 /opt,那么它之后所有层的缓存 key 都变了,Docker 只能从头往下重跑:重新拷贝 jdk 包、重新解压、重新拷 jar。这就是为什么"固定不变的东西往上放,天天变的东西往下放"不是风格偏好,而是实打实的构建时间。
坑三:docker history 里全是 <missing>。 用 BuildKit 或者拉取别人构建的镜像时很常见,表示本地拿不到那层的构建指令元数据。它不影响运行,只是看不到"这一层是哪条命令产生的"。想看完整信息,自己构建时加 --no-cache 重跑,或者用 docker history --no-trunc 打全 CREATED BY。
和下一篇的边界
这篇只回答一个问题:层是怎么来的,为什么这么叠 。至于 Dockerfile 里 FROM、COPY、RUN、ENV、ENTRYPOINT 各自有哪些参数、ADD 和 COPY 有什么区别、CMD 和 ENTRYPOINT 怎么配合------那是语法层面的东西,留给「自定义镜像-Dockerfile」一篇。这里只需要记住一句:每一个指令都会形成一层 Layer,中间部分不确定,两头一定是 BaseImage 和 Entrypoint。
API 速览
| 命令 | 看什么 | 关键点 |
|---|---|---|
docker build -t 名称:版本 . |
从 Dockerfile 构建镜像 | 末尾的 . 是构建上下文目录 |
docker history 镜像 |
逐层列出指令、大小、创建时间 | 0B + #(nop) 表示元数据层;<missing> 是缺元数据,非报错 |
docker history --no-trunc 镜像 |
完整显示 CREATED BY |
指令被截断时用 |
docker inspect -f '{``{.RootFS.Layers}}' 镜像 |
直接打印层 ID 列表 | 顺序自下而上,前几项相同即共享 base |
docker image prune |
清理无标签的悬空层 | 只删没被引用的,不影响在用镜像 |
官方文档
总结
镜像的分层不是设计美学,是被三个现实问题逼出来的:多版本之间要能复用、多个镜像之间要能共享、构建要能走缓存。
记住三条就够用了:
- 一切从 BaseImage 开始往上叠,一层只做一件事,最上面永远有一个 Entrypoint。
- 每层只读,容器启动时在最顶上挂一个可写层,写操作先复制再改。
- 变化越频繁的内容在 Dockerfile 里放得越靠后,构建缓存命中率才高。
下一篇动真格:用 Dockerfile 把一个 Java 项目真的构建成镜像跑起来。