Docker—5章(Dockerfile)


第五章 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可查看到LABELMAINTAINER的内容。

1.4 ENV

【语法1】ENV <key> <value>

【解析】用于指定环境变量,这些环境变量,后续可以被RUN指令使用,容器运行起来之后,也可以在容器中获取这些环境变量。


【语法2】ENV <key1>=<value1> <key2>=<value2>...

【解析】可以设置多个变量,每个变量为一对<key>=<value>指定。

1.5 WORKDIR

【语法】WORKDIR path

【解析】容器打开后默认进入的目录,一般在后续的RUNCMDENTRYPOINTADD等指令中会引用该目录。可以设置多个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

编译hello.c文件

运行hello

2.1.5 创建 Dockerfile

hw 目录中新建 Dockerfile,内容如下:

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

2.1.6 构建镜像

docker build -t hello-my-world .

-t 用于指定要生成的镜像的<repository><tag>。若省略 tag,则默认为 latest

最后的点(.)是一个宿主机的 URL 路径,构建镜像时会从该路径中查找 Dockerfile 文件。同时该路径也是在 Dockerfile ADDCOPY 指令中若使用的是相对路径,那个相对路径就相对的这个路径。不过需要注意,即使 ADDCOPY 指令中使用绝对路径来指定源文件,该源文件所在路径也必须要在这个 URL 指定目录或子目录内,否则将无法找到该文件。

通过 docker images 查看本地镜像,可以看到新构建的 hello-my-world 镜像。

2.1.7 运行新镜像

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

2.1.8 为镜像重打标签

某镜像被指定为 latest 后,后期又出现了更新的版本需要被指定为 latest,那么原 latest镜像就应被重打<tag>标签,否则,当最新版被发布为 latest 后,原镜像就会变为悬虚镜像

通过 docker tag 命令可对镜像重打标签。所谓重打标签,实际是复制了一份原镜像,并为新的镜像指定新的<tag>。当然,重新指定<repository>也是可以的。所以,新镜像的 ImageIDDigest 都与原镜像的相同。

2.2 构建自己的Centos镜像

从镜像中心拉取来的 centos:7 镜像中是没有 vimifconfigwget 等常用命令的,这里要构建一个自己的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 了,ifconfigvim 命令可以使用了。

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 不会被覆盖

    运行结果会报错,系统认为 datecal 的非法参数。

  • 添加命令选项生效

    与之前不同的是,这种情况下在 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]指定的 shelldaemon 会直接运行,而不会与 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 目录中发现自动生成两个目录 addcopy

分别查看这两个目录发现,通过 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 build
  • docker commit
  • docker import(注意,docker load 并没有构建出新的镜像,其与原镜像是同一个镜像)
  • docker compose
  • docker 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 命令可以清除。


文章作者: 念心卓
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 念心卓 !
  目录