Java 项目部署之 Docker工具快速入门: 自定义镜像的分层结构与写时复制

概述

自己写的 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 只读
命中,返回文件
写文件
目标层可写?
复制到可写层再修改

下层原文件不动
直接修改

这个设计带来两个直接后果:

  1. 同一个镜像可以同时跑 N 个容器,大家共用下面所有只读层,只有最上面各自一个薄薄的可写层。省的是磁盘,也是内存里共享的文件页。
  2. 容器删掉,只删它自己的可写层,镜像本身毫发无损。所以"容器不能持久化数据"这个坑,得靠数据卷挂载来解决。

层复用的两个收益

分层的价值不止是"好看",实测下来主要吃两头红利。

一、多个镜像共享 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 项目真的构建成镜像跑起来。

相关推荐
kimnoic9 小时前
Python数据库学习心得:SQLite、MySQL、PostgreSQL优缺点
开发语言·python
别动我齐刘海10 小时前
C++ 工程化学习
java·linux·前端·c++·学习·机器学习·机器人
必须得开心呀10 小时前
python编写带界面的软件,需要安装哪些插件
开发语言·python
code2cat11 小时前
Java进阶篇之AtomicLongFieldUpdater:把累计额度留在对象字段中
java·cas·并发编程
WeiXin_DZbishe11 小时前
基于springboot城市公交实时运营与调度系统-计算机毕设 附源码【课程设计】66173
java·vue.js·spring boot·后端·python·php·课程设计
泡茶喝茶写代码11 小时前
A股量化数据工程:从 REST 接口到策略信号(第 18 篇):放量缩量异动扫描
java·python·股票数据api·股票数据api接口·股票api数据接口·股票量化数据api·股票量化数据接口
wewowilling12 小时前
认识微服务
java·运维·微服务·springcloud