第五章 Dockerfile
Dockerfile是用于构建Docker镜像的脚本文件,由一系列指令构成。通过docker build命令构建镜像时,Dockerfile中的指令会由上到下依次执行,每条指令都将会构建出一个镜像。这就是镜像的分层。因此,指令越多,层次就越多,创建的镜像就越多,效率就越低。所以在定义Dockerfile时,能在一个指令完成的动作就不要分为两条。
1. 指令简介
对于Dockerfile的指令,需要注意以下几点:
- 指令是大小不敏感的,但惯例是写为全大写。
- 指令后至少会携带一个参数。
#号开头的行为注释。
1.1 FROM
【语法】FROM <image>[:<tag>]
【解析】用于指定基础镜像,且必须是第一条指令;若省略了tag,则默认为latest。
1.2 MAINTAINER
【语法】MAINTAINER <name>
【解析】MAINTAINER指令的参数填写的一般是维护者姓名和信箱。不过,该指令官方已不建议使用,而是使用LABEL指令代替。
1.3 LABEL
【语法】LABEL<key>=<value> <key>=<value>.....
【解析】LABEL指令中可以以键值对的方式包含任意镜像的元数据信息,用于替代MAINTAINER指令。通过docker inspect可查看到LABEL与MAINTAINER的内容。
1.4 ENV
【语法1】ENV <key> <value>
【解析】用于指定环境变量,这些环境变量,后续可以被RUN指令使用,容器运行起来之后,也可以在容器中获取这些环境变量。
【语法2】ENV <key1>=<value1> <key2>=<value2>...
【解析】可以设置多个变量,每个变量为一对<key>=<value>指定。
1.5 WORKDIR
【语法】WORKDIR path
【解析】容器打开后默认进入的目录,一般在后续的RUN、CMD、ENTRYPOINT、ADD等指令中会引用该目录。可以设置多个WORKDIR指令。后续WORKDIR指令若用的是相对路径,则会基于之前WORKDIR指令指定的路径。在使用docker run运行容器时,可以通过-w参数覆盖构建时所设置的工作目录。
1.6 RUN
【语法1】RUN <command>
【解析】这里的<command>就是shell命令。docker build执行过程中,会使用shell运行指定的command。
【语法2】RUN ["EXECUTABLE","PARAM1","PARAM2", ...]
【解析】在docker build执行过程中,会调用第一个参数"EXECUTABLE"指定的应用程序运行,并使用后面第二、三等参数作为应用程序的运行参数。
1.7 CMD
【语法1】CMD ["EXECUTABLE","PARAM1","PARAM2", ...]
【解析】在容器启动后,即在执行完docker run后会立即调用执行"EXECUTABLE"指定的可执行文件,并使用后面第二、三等参数作为应用程序的运行参数。
【语法2】CMD command param1 param2, ...
【解析】这里的command就是shell命令。在容器启动后会立即运行指定的shell命令。
【语法3】CMD ["PARAM1","PARAM2", ...]
【解析】提供给ENTERYPOINT的默认参数。
1.8 ENTRYPOINT
【语法 1】ENTRYPOINT ["EXECUTABLE","PARAM1","PARAM2", ...]
【解析】在容器启动过程中,即在执行 docker run 时,会调用执行”EXECUTABLE“指定的应用程序,并使用后面第二、三等参数作为应用程序的运行参数。
【语法 2】ENTRYPOINT command param1 param2, ...
【解析】这里的 command 就是 shell 命令。在容器启动过程中,即在执行 docker run 时,会运行指定的 shell 命令。
1.9 EXPOSE
【语法】EXPOSE <port> [<port>.
【解析】指定容器准备对外暴露的端口号,但该端口号并不会真正的对外暴露。若要真正暴露,则需要在执行 docker run 命令时使用-p(小 p)来指定说要真正暴露出的端口号。
1.10 ARG
【语法】ARG < varname >[=<default value
【解析】定义一个变量,该变量将会使用于镜像构建运行时。若要定义多个变量,则需要定义多个 ARG 指令。
1.11 ADD
【语法 1】ADD <src> <dest>
【语法 2】ADD ["<src>", "<dest>"] # 路径中存在空格时使用双引号引起来
【解析】该指令将复制当前宿主机中指定文件 src 到容器中的指定目录 dest 中。
src 可以是宿主机中的绝对路径,也可以时相对路径。但相对路径是相对于 docker build 命令所指定的路径的。
src 指定的文件可以是一个压缩文件,压缩文件复制到容器后会自动解压为目录;
src 也可以是一个 URL,此时的 ADD 指令相当于 wget 命令;
src 最好不要是目录,其会将该目录中所有内容复制到容器的指定目录中。dest 是一个绝对路径,其最后面的路径必须要加
上斜杠,否则系统会将最后的目录名称当做是文件名的。
1.12 COPY
【说明】功能与 ADD 指令相同,只不过 src 不能是 URL。若 src 为压缩文件,复制到容器后不会自动解压。
1.13 ONBUILD
【语法】ONBUILD [INSTRUCTION]
【解析】该指令用于指定当前镜像的子镜像进行构建时要执行的指令。
1.14 VOLUME
【语法】VOLUME ["dir1", “dir2”, ...]
【解析】在容器创建可以挂载的数据卷。
2. 指令用法
2.1 构建自己的HelloWorld镜像
2.1.1 scratch 镜像
在构建自己的镜像之前,首先要了解一个特殊的镜像 scratch。scratch 镜像是一个空镜像,是所有镜像的 Base Image(相当于面向对象编程中的 Object类)。scratch 镜像只能在 Dockerfile 中被继承,不能通过 pull 命令拉取,不能 run,也没有 tag。并且它也不会生成镜像中的文件系统层。在 Docker 中,scratch 是一个保留字,用户不能作为自己的镜像名称使用。
2.1.2 安装编译器
由于下面要编写、编译一段 C 语言代码,所以这里先安装一下 C 语言的编译器。
yum install -y gcc gcc-c++
由于后面在编译时要使用 C 的静态库,所以要再安装 glibc-static。
yum install -y glibc-static

2.1.3 创建 hello.c
在宿主机任意目录创建一个名称为hello.c的文件。这里在/root/tempdoc/ 下 mkdir 一个目录 hw,然后将hello.c文件创建在这里。文件内容如下:
#include<stdio.h>
int main(){
printf("hello my docker world\n");
return 0;
}
2.1.4 编译测试 hello.c
使用 gcc 编译hello.c文件。
gcc --static -o hello hello.c


2.1.5 创建 Dockerfile
在 hw 目录中新建 Dockerfile,内容如下:
FROM scratch
ADD hello /
CMD ["/hello"]
- FROM scratch:这是指定基础镜像的指令。”scratch” 是一个特殊的基础镜像,表示从一个空白的镜像开始构建。通常用于构建最小化的、精简的镜像。
- ADD hello /:这是将本地文件或目录添加到镜像中的指令。在这里,它将当前目录下的文件 “hello” 添加到镜像的根目录下。
- CMD [“/hello”]:这是定义容器启动时要执行的默认命令或可执行文件的指令。在这里,它指定容器启动后要执行的命令是 “/hello”,也就是在镜像中添加的 “hello” 可执行文件。
2.1.6 构建镜像
docker build -t hello-my-world .

-t 用于指定要生成的镜像的<repository>与<tag>。若省略 tag,则默认为 latest。
最后的点(.)是一个宿主机的 URL 路径,构建镜像时会从该路径中查找 Dockerfile 文件。同时该路径也是在 Dockerfile 中 ADD、COPY 指令中若使用的是相对路径,那个相对路径就相对的这个路径。不过需要注意,即使 ADD、COPY 指令中使用绝对路径来指定源文件,该源文件所在路径也必须要在这个 URL 指定目录或子目录内,否则将无法找到该文件。
通过 docker images 查看本地镜像,可以看到新构建的 hello-my-world 镜像。

2.1.7 运行新镜像
在任意目录下都可运行该镜像。

2.1.8 为镜像重打标签
某镜像被指定为 latest 后,后期又出现了更新的版本需要被指定为 latest,那么原 latest镜像就应被重打<tag>标签,否则,当最新版被发布为 latest 后,原镜像就会变为悬虚镜像。
通过 docker tag 命令可对镜像重打标签。所谓重打标签,实际是复制了一份原镜像,并为新的镜像指定新的<tag>。当然,重新指定<repository>也是可以的。所以,新镜像的 ImageID、Digest 都与原镜像的相同。

2.2 构建自己的Centos镜像
从镜像中心拉取来的 centos:7 镜像中是没有 vim、ifconfig、wget 等常用命令的,这里要构建一个自己的centos7镜像,使这些命令都可以使用。
2.2.1 创建 Dockerfile
在宿主机任意目录创建一个文件,并命名为 Dockerfile。这里在/root/tempdoc/ 下 mkdir 一个目录dfs。然后将如下内容复制Dockerfile到文件中:
FROM centos:7 #前提是这个镜像要存在
MAINTAINER zhangsan zs@163.com
LABEL version="1.0" description="this is a custom centos image"
ENV WORKPATH /usr/local
WORKDIR $WORKPATH
RUN yum -y install vim net-tools wget #安装net-tools 使其可以使用ifconfig、vim等命令
CMD /bin/bash
2.2.2 构建镜像 build

此时通过 docker images 命令可以查看到刚刚生成的新的镜像。并且还发现,新镜像的大小要大于原镜像的,因为新镜像安装了新软件。

2.2.3 运行新建镜像
运行了新镜像后,发现默认路径是/usr/local 了,ifconfig、vim 命令可以使用了。

2.3 悬虚镜像
悬虚镜像是指既没有 Repository 又没有 Tag 的镜像。当新建了一个镜像后,为该镜像指定了一个已经存在的 TAG,那么原来的镜像就会变为悬空镜像。
为了演示悬虚镜像的生成过程,这里先修改前面定义的 Dockerfile,然后再生成镜像,且生成的新的镜像与前面构建的镜像的名称与 Tag 均相同。
2.3.1 修改 Dockerfile
修改/root/tempdoc/dfs 中的 Dockerfile。修改任意内容均可。这里仅将原来的 LABEL 中的 version值由 1.0 修改为了 2.0,其它没变。
FROM centos:7 #前提是这个镜像要存在
MAINTAINER zhangsan zs@163.com
LABEL version="2.0" description="this is a custom centos image"
ENV WORKPATH /usr/local
WORKDIR $WORKPATH
RUN yum -y install vim net-tools wget #安装net-tools 使其可以使用ifconfig、vim等命令
CMD /bin/bash
2.3.4 构建镜像 build
在构建镜像之前,先查看前面构建的 cucentos:1.0 镜像的 ID,以备在后面进行对比。

构建镜像时仍然指定镜像为 cucentos:1.0,与前面的镜像完全重名。

构建完毕后,再次查看镜像,发现原来 cucentos:1.0 镜像的名称与 Tag 均变为了<none>,即变为了悬虚镜像。

2.3.5 删除悬虚镜像
悬虚镜像是一种“无用”镜像,其存在只能是浪费存储空间,所以一般都是要删除的。对于悬虚镜像的删除,除了可以通过 docker rmi <imageID>进行删除外,还有专门的删除命令 docker image prune。该命令能够一次性删除本地全部的悬空镜像。不过有个前提,就是这些悬虚镜像不能是已经启动了容器的,无论容器是否是退出状态。当然,如果再加上-a选项,则会同时再将没有被任何容器使用的镜像也删除。
另外,还有一个命令 docker system prune 也可以删除悬虚镜像。只不过,其不仅删除的是悬虚镜像,还有其它系统“无用”内容。
在删除这个悬虚镜像之前,首先查看其是否启动了容器。如果启动了,则先将容器删除。

在删除了相关容器后再运行 docker image prune。

此时再查看就发现悬虚镜像已经被删除了。

使用 docker system prune 命令可删除系统中的四类“无用”内容,其中就包含悬虚镜像dangling images。

2.4 CMD与ENTERYPOINT用法
这两个指令都用于指定容器启动时要执行的命令,无论哪个指令,每个 Dockerfile 中都只能有一个 CMD/ENTERYPOINT 指令,多个 CMD/ENTERYPOINT 指令只会执行最后一个。不同的是,CMD 指定的是容器启动时默认的命令,而 ENTRYPOINT 指定的容器启动时一定会执行的命令。即 docker run 时若指定了要运行的命令,Dockerfile 中的 CMD 指令指定的命令是不会执行的,而 ENTERYPOINT 中指定的命令是一定会执行的。
2.4.1 CMD-shell
创建 Dockerfile
在
dfs目录中新建文件Dockerfile2,并定义内容如下。FROM centos:7 CMD cal构建镜像 build

说明:
-f用于指定本次构建所要使用的Dockerfile文件。如果文件名不是指定的,则docker build默认加载的Dockerfile这个名称。
运行新建镜像
运行后可以查看到当前月份的日历。

覆盖 CMD

在
docker run命令中指定要执行的命令,Dockerfile中通过CMD指定的默认的命令就不会在执行。不能添加命令选项

这种方式无法为
CMD中指定的默认的命令指定选项。
2.4.2 CMD-exec
创建 Dockerfile
在
dfs目录中新建文件Dockerfile3,并将如下内容复制到文件中。FROM centos:7 CMD ["/bin/bash", "-c", "cal"]构建镜像 build
使用
Dockerfile3构建镜像mycal:2.0。
运行新建镜像
运行结果与
shell方式的相同
覆盖 CMD
运行结果与
shell方式的相同,也可以被覆盖。
不能添加命令选项
虽然在
CMD中指定可以从命令行接收选项,但运行结果与shell方式的相同,也不能添加命令选项。这是由CMD命令本身决定的。
2.4.3 ENTRYPOINT-shell
创建 Dockerfile
在
dfs目录中新建文件Dockerfile4,并将如下内容复制到文件中。FROM centos:7 ENTRYPOINT cal构建镜像 build
使用
Dockerfile4构建镜像mycal:3.0。
运行新建镜像

ENTRYPOINT 不会被覆盖
ENTRYPOINT指定的命令是不会被docker run中指定的命令给覆盖掉的。
添加命令选项无效
在
docker run中添加的命令选项,对于ENTRYPOINT中指定的命令是无效的。在这点上不像CMD指令一样报错。
2.4.4 ENTRYPOINT-exec
创建 Dockerfile
在
dfs目录中新建文件Dockerfile5,并将如下内容复制到文件中。FROM centos:7 ENTRYPOINT ["cal"]构建镜像 build
使用
Dockerfile5构建镜像mycal:4.0。
运行新建镜像
运行结果与 shell 方式的相同。

ENTRYPOINT 不会被覆盖
运行结果会报错,系统认为
date是cal的非法参数。
添加命令选项生效
与之前不同的是,这种情况下在
docker run中添加的命令选项是有效的。
2.4.5 ENTRYPOINT 与 CMD 同用
创建 Dockerfile
在
dfs目录中新建文件Dockerfile6,并将如下内容复制到文件中。此时的CMD中给出的就是ENTRYPOINT的参数,注意不能是选项。FROM centos:7 CMD ["hello world"] ENTRYPOINT ["echo"]构建镜像 build
使用 Dockerfile6 构建镜像 myecho:latest。


运行新建镜像

添加命令选项生效
在
docker run –it myecho命令后添加选项>hello.log,用于将输出的内容重定向写入到hello.log文件中。选项生效。
覆盖 CMD 生效
在
docker run –it myecho命令后指定新的参数,用于覆盖CMD中的参数,生效。
2.4.6 总结
Dockerfile 中的[command]或[“EXECUTABLE”]如果是通过 CMD 指定的,则该镜像的启动命令 docker run 中是不能添加参数[ARG]的。因为 Dockerfile 中的 CMD 是可以被命令中的[COMMAND]替代的。如果命令中的 IMAGE 后仍有内容,此时对于 docker daemon 来说,其首先认为是替代用的[COMMAND],如果有两个或两个以上的内容,后面的内容才会认为是[ARG]。所以,添加的-y 会报错,因为没有-y 这样的[COMMAND]。
Dockerfile 中的[command]或[“EXECUTABLE”]如果是通过 ENTRYPOINT 指定的,则该镜像的启动命令 docker run 中是可以添加参数[ARG]的。因为 Dockerfile 中的 ENTRYPOINT 是不能被命令中的[COMMAND]替代的。如果命令中的 IMAGE 后仍有内容,此时对于 docker daemon来说,其只能是[ARG]。
不过,docker daemon 对于 ENTRYPOINT 指定的[command]与[“EXECUTABLE”]的处理方式是不同的。如果是[command]指定的 shell,daemon 会直接运行,而不会与 docker run 中的[ARG]进行拼接后运行;如果是[“EXECUTABLE”]指定的命令,daemon 则会先与 docker run 中
的[ARG]进行拼接,然后再运行拼接后的结果。
结论:无论是 CMD 还是 ENTRYPOINT,使用[“EXECUTABLE”]方式的通用性会更强些。
2.5 ADD和COPY指令
2.5.1 准备工作
在宿主机/root/tempdoc/ 目录中 mkdir 一个目录 ac。将事先下载好的任意某 tar.gz 包上传到/root/tempdoc/ac目录。本例在 zookeeper 官网 https://zookeeper.apache.org 下载了 zookeeper 的tar.gz压缩包,在上传到/root/tempdoc/ac 目录后,为了方便又重命名了这个压缩包为 zookeeper.tar.gz。

2.5.2 创建 Dockerfil
在/root/tempdoc/ac 目录中新建文件 Dockerfile,内容如下:
FROM centos:7
WORKDIR /opt
ADD zookeeper.tar.gz /opt/add/
COPY zookeeper.tar.gz /opt/copy/
CMD /bin/bash
2.5.3 构建镜像 build
使用 Dockerfile 构建镜像 addcopy。

2.5.4 运行新建镜像
启动 addcopy 镜像,在容器的/opt 目录中发现自动生成两个目录 add与 copy

分别查看这两个目录发现,通过 ADD 指令添加的是解压过的目录,而通过 COPY 指令添加的是未解压的。这就是 ADD 与 COPY 指令的区别。

2.6 ARG指令
该指令用于定义一个变量,该变量将会在镜像构建时使用。注意不是容器启动时,容器启动时镜像构建早已完成。
2.6.1 创建 Dockerfile
mkdir 一个名称为 arg 的目录,在其中新建文件 Dockerfile,内容如下:
FROM centos:7
ARG name=Tom
RUN echo $name
RUN 指令用于指定在 docker build 执行时要执行的内容。
2.6.2 使用 ARG 默认值构建
使用 Dockerfile 构建镜像 myargs:1.0。我们可以看到,在镜像构建时读取了 ARG 中参数,只不过docker build中并没有给变量 name 赋予新值,所以 name 使用的是其默认值 Tom。

2.6.3 使用 ARG 指定值构建
使用 Dockerfile 构建镜像 myargs:2.0。在 docker build 命令中指定了 ARG 中参数值,覆盖了默认值。

2.7 ONBUILD指令
ONBUILD 指令只对当前镜像的子镜像进行构建时有效。
下面实现的需求是:父镜像中没有 wget 命令,但子镜像中会增加。
2.7.1 创建父镜像操作
创建父镜像 Dockerfile
mkdir一个名称为onbuild的目录,并在其中新建文件Dockerfile,内容如下:FROM centos:7 ENV WORKPATH /usr/local WORKDIR $WORKPATH ONBUILD RUN yum -y install wget CMD /bin/bash当前镜像及其将来子镜像的工作目录都将是
/usr/local,将来以交互模式运行后都会直接进入到bash命令行。ONBUILD中指定要安装的wget命令,是在子镜像进行docker build时会RUN的安装命令。构建父镜像
使用
Dockerfile构建镜像parent:1.0。
运行父镜像
运行父镜像我们发现,其工作目录为
/usr/local,且没有wget命令。
2.7.2 创建子镜像操作
创建子镜像 Dockerfile
在
onbuild目录中新建文件Dockerfile2,内容仅包含一句话,指定父镜像FROM parent:1.0构建子镜像
子镜像在构建过程中下载了
wget命令。
运行子镜像
我们发现子镜像不仅能够直接进入到
bash命令行,工作目录为/usr/local,其还直接具有wget命令。而这些功能除了继承自父镜像外,就是在构建过程中来自于ONBUILD指定的指令。
2.8 构建新镜像的方式总结
可以构建出新的镜像的方式有:
docker builddocker commitdocker import(注意,docker load并没有构建出新的镜像,其与原镜像是同一个镜像)docker composedocker hub中完成Automated Builds
3. 应用发布
开发出的应用程序如何通过 Dockerfile 部署到 Docker 容器中?下面就通过将一个 Spring Boot 应用部署到 Docker 为例来说明这个部署过程。
3.1 准备应用
下面的应用就是一个名称为 hello-docker 的最简单的Spring Boot工程。
3.1.1 定义 POM
该 POM 文件就是一个基本的 Spring Boot 工程的依赖文件,没有其它特殊的内容。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.docker</groupId>
<artifactId>hello-docker</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>hello-docker</name>
<description>hello-docker</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3.1.2 定义配置文件
application.yml 文件中仅设置了 logback 日志输出格式。采用默认端口号 8080。
# 配置Logback日志控制
logging:
pattern:
console: Level-%-5Level %msg%n
3.1.3 定义启动类
启动类为 HelloDockerApplication
package com.docker.hellodocker;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class HelloDockerApplication {
public static void main(String[] args) {
SpringApplication.run(HelloDockerApplication.class, args);
}
}
3.1.4 定义 Controll
Controller 类为 HelloController。
@RestController
public class HelloController {
@GetMapping("/hello")
public String helloHandler(){
return "hello Dockerfile world";
}
}
3.1.5 打包Jar
将应用打为 Jar 包,以备后面使用。

3.2 发布应用
3.2.1 准备目录
在宿主机中需要为应用创建一个专门的目录。该目录不仅用于存放应用的 Jar 包,还用于存放应用的 Dockerfile 文件与数据卷目录。目录名随意,一般为项目名称。本例在/root/tempdoc 下 mkdir 一个目录 hello-docker,并将前面应用打好的 Jar 包上传到该目录
3.2.2 创建 Dockerfile
在/root/tempdoc/hello-docker 目录中创建 Dockerfile 文件,文件内容如下:
FROM openjdk:8u102
MAINTAINER zhangsan zs@163.com
LABEL version="1.0" description="my own app"
COPY hello-docker-0.0.1-SNAPSHOT.jar hd.jar
ENTRYPOINT ["java", "-jar", "hd.jar"]
EXPOSE 9000
3.2.3 构建镜像

3.2.4 运行容器
以分离模式运行容器

3.3 访问
在浏览器中直接访问 docker 宿主机就可以访问到应用了。

再通过 docker logs 命令可查看到输出的日志。

4. build cache
4.1 测试环境构建
为了了解什么是 build cache,理解镜像构建过程中 build cache 机制,这里需要先搭建一个测试环境。
4.1.1 新建 hello.log
在/root/tempdoc/ 下 mkdir 一个目录 cache,在其中新建 hello.log 文件。

4.1.2 Dockerfile 举例
在/root/tempdoc/cache 中创建一个 Dockerfile 文件,内容如下:
FROM centos:7
LABEL auth="Tom"
COPY hello.log /var/log/
RUN yum -y install vim
CMD /bin/bash
4.1.3 第一次构建镜像
这是使用前面的 Dockerfile 第一次构建镜像。

4.2 镜像生成过程
为了了解什么是 build cache,理解镜像构建过程中 build cache 机制,需要先了解镜像的生成过程。
Docker 镜像的构建过程,大量应用了镜像间的父子关系。即下层镜像是作为上层镜像的父镜像出现,下层镜像是作为上层镜像的输入出现;上层镜像是在下层镜像的基础之上变化而来。下面将针对上面的例子逐条指令的分析镜像的构建过程。
4.2.1 FROM centos:7
FROM 指令是 Dockerfile 中唯一不可缺少的指令,它为最终构建出的镜像设定了一个基础镜像(Base Image)。该语句并不会产生新的镜像层,它是使用指定的镜像作为基础镜像层的。docker build 命令解析 Dockerfile 的 FROM 指令时,可以立即获悉在哪一个镜像基础上完成下一条指令镜像层构建。
对于本例,Docker Daemon 首先从 centos:7 镜像的文件系统获取到该镜像的 ID,然后再根据镜像 ID 提取出该镜像的 json 文件内容,以备下一条指令镜像层构建时使用。
4.2.2 LABEL auth=”Tom”
LABEL 指令仅修改上一步中提取出的镜像 json 文件内容,在 json 中添加 LABEL auth="Tom",无需更新镜像文件系统。但也会生成一个新的镜像层,只不过该镜像层中只记录了 json 文件内容的修改变化,没有文件系统的变化。
如果该指令就是最后一条指令,那么此时形成的镜像的文件系统其实就是原来 FROM 后指定镜像的文件系统,只是 json 文件发生了变化。但由于 json 文件内容发生了变化,所以产生了新的镜像层。
4.2.3 COPY hello.log /var/log/
COPY 指令会将宿主机中的指定文件复制到容器中的指定目录,所以会改变该镜像层文件系统大小,并生成新的镜像层文件系统内容。所以 json 文件中的镜像 ID 也就发生了变化,产生了新的镜像层。
4.2.4 RUN yum -y install vim
RUN 指令本身并不会改变镜像层文件系统大小,但由于其 RUN 的命令是 yum install,而该命令运行的结果是下载并安装一个工具,所以导致 RUN 命令最终也改变了镜像层文件系统大小,所以也就生成了新的镜像层文件系统内容。所以 json 文件中的镜像 ID 也就发生了变化,产生了新的镜像层。
4.2.5 CMD /bin/bash
对于 CMD 或 ENTRYPOINT 指令,其是不会改变镜像层文件系统大小的,因为其不会在docker build 过程中执行。所以该条指令没有改变镜像层文件系统大小。
但对于 CMD 或 ENTRYPOINT 指令,由于其是将来容器启动后要执行的命令,所以会将该条指令写入到 json 文件中,会引发 json 文件的变化。所以 json 文件中的镜像 ID 也就发生了变化,产生了新的镜像层。
4.3 修改Dockerfile后重新构建
4.3.1 修改 Dockerfile
在前面 Dockerfile 中再增加一条 EXPOSE 指令。
FROM centos:7
LABEL auth="Tom"
COPY hello.log /var/log/
RUN yum -y install vim
CMD /bin/bash
EXPOSE 9000
4.3.2 构建新镜像
此时再构建新的镜像 test:2.0,会发现没有下载安装 vim 的过程了,但发现了很多的 CACHED。说明这是使用了 build cache。

各自查看它们的 docker history。

发现 test:2.0 中的镜像层,除了新增加指令 EXPOSE 镜像层外,其它层完全与 test:1.0 的相同。test:2.0 在构建时复用了 test:1.0 的镜像层。
4.3.3 删除 test:1.0 镜像
此时将test:1.0镜像删除。
4.3.4 再构建新镜像
再次构建test:3.0镜像。发现仍然使用了大量的 build cache,就连 EXPOSE 指令镜像也使用了 build cache。

4.4 build cache机制
Docker Daemnon 通过 Dockerfile 构建镜像时,当发现即将新构建出的镜像(层)与本地已存在的某镜像(层)重复时,默认会复用已存在镜像(层)而不是重新构建新的镜像(层),这种机制称为 docker build cache 机制。该机制不仅加快了镜像的构建过程,同时也大量节省了Docker 宿主机的空间。
docker build cache 并不是占用内存的 cache,而是一种对磁盘中相应镜像层的检索、复用机制。所以,无论是关闭 Docker 引擎,还是重启 Docker 宿主机,只要该镜像(层)存在于本地,那么就会复用。
4.5 build cache失效
docker build cache 在以下几种情况下会失效。
4.5.1 Dockerfile 文件发生变化
当 Dockerfile 文件中某个指令内容发生变化,那么从发生变化的这个指令层开始的所有镜像层 cache 全部失效。即从该指令行开始的镜像层将构建出新的镜像层,而不再使用 build cache,即使后面的指令并未发生变化。因为镜像关系本质上是一种树状关系,只要其上层节点变了,那么该发生变化节点的所有下层节点也就全部变化了。
4.5.2 ADD 或 COPY 指令内容变化
Dockerfile 文件内容没有变化,但 ADD 或 COPY 指令所复制的文件内容发生了变化,同样会使从该指令镜像层开始的后面所有镜像层的 build cache 失效。
4.5.3 RUN 指令外部依赖变化
与 ADD/COPY 指令相似。Dockerfile 文件内容没有变化,但 RUN 命令的外部依赖发生了变化,例如本例中要安装的 vim 软件源发生了变更(版本变化、下载地址变化等),那么从发生变化的这个指令层开始的所有镜像层 cache 全部失效。
4.5.4 指定不使用 build cache
有些时候为了确保在镜像构建过程中使用到新的数据,在镜像构建 docker build 时,通过--no-cache 选项指定不使用 build cache。

4.6 清理dangling build cache
dangling build cache,即悬虚 build cache,指的是无法使用的 build cache。一般为悬虚镜像 dangling image 所产生的 build cache。通过 docker system prune 命令可以清除。
