Dockerfile详解

Dockerfile文件

Dockerfile 是用于构建 Docker 镜像的脚本文件,由一系列指令构成。通过 docker build命令构建镜像时,Dockerfile 中的指令会由上到下依次执行,每条指令都将会构建出一个镜像。这就是镜像的分层。因此,指令越多,层次就越多,创建的镜像就越多,效率就越低。所以在定义 Dockerfile 时,能在一个指令完成的动作就不要分为两条。

Dockerfile 指令

  1. FROM

    【语法】FROM <image>[:<tag>]

    【解析】用于指定基础镜像,且必须是第一条指令;若省略了 tag,则默认为 latest。

  2. MAINTAINER

    【语法】MAINTAINER <name>

    【解析】MAINTAINER 指令的参数填写的一般是维护者姓名和信箱。不过,该指令官方已不建议使用,而是使用 LABEL 指令代替。

  3. LABEL

    【语法】LABEL <key>=<value> <key>=<value>

    【解析】LABEL 指令中可以以键值对的方式包含任意镜像的元数据信息,用于替代MAINTAINER 指令。通过 docker inspect 可查看到 LABEL 与 MAINTAINER 的内容。

  4. ENV

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

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

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

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

  5. WORKDIR

    【语法】WORKDIR path

    【解析】容器打开后默认进入的目录,一般在后续的 RUN、CMD、ENTRYPOINT、ADD 等指令中会引用该目录。可以设置多个 WORKDIR 指令。后续 WORKDIR 指令若用的是相对路径,则会基于之前 WORKDIR 指令指定的路径。在使用 docker run 运行容器时,可以通过-w 参数覆盖构建时所设置的工作目录。

  6. RUN

    【语法 1】RUN <command>

    【解析】这里的<command>就是 shell 命令。docker build 执行过程中,会使用 shell 运行指定的 command。

    【语法 2】RUN [ " EXECUTABLE " , " PARAM1 " , " PARAM2 " , …]

    【解析】在 docker build 执行过程中,会调用第一个参数"EXECUTABLE"指定的应用程序运行,并使用后面第二、三等参数作为应用程序的运行参数。

  7. CMD

    【语法 1】CMD [ " EXECUTABLE " , " PARAM1 " , " PARAM2 " , …]

    【解析】在容器启动后,即在执行完 docker run 后会立即调用执行"EXECUTABLE"指定的可执行文件,并使用后面第二、三等参数作为应用程序的运行参数。

    【语法 2】CMD command param1 param2, …

    【解析】这里的 command 就是 shell 命令。在容器启动后会立即运行指定的 shell 命令。

    【语法 3】CMD [" PARAM1 " , " PARAM2 " , …]

    【解析】当CMD和ENTRYPOINT指令一起使用时,CMD可以提供参数给ENTRYPOINT。

  8. ENTRYPOINT

    【语法 1】ENTRYPOINT [ " EXECUTABLE " , " PARAM1 " , " PARAM2 " , …]

    【解析】在容器启动过程中,即在执行 docker run 时,会调用执行"EXECUTABLE"指定的应用程序,并使用后面第二、三等参数作为应用程序的运行参数。

    【语法 2】ENTRYPOINT command param1 param2, …

    【解析】这里的 command 就是 shell 命令。在容器启动过程中,即在执行 docker run 时,会运行指定的 shell 命令。

  9. EXPOSE

    【语法】EXPOSE <port> [<port>…]

    【解析】指定容器准备对外暴露的端口号,但该端口号并不会真正的对外暴露。若要真正暴露,则需要在执行 docker run 命令时使用-p(小 p)来指定说要真正暴露出的端口号。

  10. ARG

    【语法】ARG < varname >[=<default value>]

    【解析】定义一个变量,该变量将会使用于镜像构建运行时。若要定义多个变量,则需要定义多个 ARG 指令。

  11. ADD

    【语法 1】ADD <src> <dest>

    【语法 2】ADD [" <src>" , " <dest>"] # 路径中存在空格时使用双引号引起来

    【解析】该指令将复制当前宿主机中指定文件 src 到容器中的指定目录 dest 中。src 可以是宿主机中的绝对路径,也可以时相对路径。但相对路径是相对于 docker build 命令所指定的路径的。src 指定的文件可以是一个压缩文件,压缩文件复制到容器后会自动解压为目录;src 也可以是一个 URL,此时的 ADD 指令相当于 wget 命令;src 最好不要是目录,其会将该目录中所有内容复制到容器的指定目录中。dest 是一个绝对路径,其最后面的路径必须要加上斜杠,否则系统会将最后的目录名称当做是文件名的。

  12. COPY

    【说明】功能与 ADD 指令相同,只不过 src 不能是 URL。若 src 为压缩文件,复制到容器后不会自动解压。

  13. ONBUILD

    【语法】ONBUILD [INSTRUCTION]

    【解析】该指令用于指定当前镜像的子镜像进行构建时要执行的指令。

  14. VOLUME

    【语法】VOLUME [" dir1 " , " dir2 ", …]

    【解析】在容器创建可以挂载的数据卷。

构建自己的hello-world镜像

scratch 镜像

在构建自己的镜像之前,首先要了解一个特殊的镜像 scratch。

scratch 镜像是一个空镜像,是所有镜像的 Base Image(相当于面向对象编程中的 Object类)。scratch 镜像只能在 Dockerfile 中被继承,不能通过 pull 命令拉取,不能 run,也没有 tag。并且它也不会生成镜像中的文件系统层。在 Docker 中,scratch 是一个保留字,用户不能作为自己的镜像名称使用。

创建和编译 hello.c

在宿主机任意目录创建一个名称为 hello.c 的文件。这里在/root 下 mkdir 一个目录 hello_world,然后将 hello.c 文件创建在这里。文件内容如下:

#include <stdio.h>

int main()
{
	printf("hello docker world!\n");
	return 0;
}

使用 gcc 编译 hello.c 文件。

image-20230909195522683

编写Dockerfile文件

在 hello_world 目录中新建一个 Dockerfile 文件,切记文件名必须是Dockerfile。内容如下:

# 表示基础镜像是scratch,也就是一个空白的镜像。
FROM scratch
# 将当前目录下的hello文件添加到镜像的根目录(/)下。
ADD hello /
# 设置了容器的默认命令,也就是当运行一个基于这个镜像的容器时,如果不指定运行的命令,那么默认会运行/hello这个命令。
CMD ["/hello"]

构建镜像

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

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

运行新镜像

image-20230909201612177

构建自己的 centos 镜像

# 新建一个Dockerfile文件
vim Dockerfile
在文件中写入如下内容
FROM centos:7
MAINTAINER bing 1234@qq.com
LABEL auth="bing" version="1.0" description="This is a Centos image I wrote myself" 
ENV WORKPATH /usr/local
WORKDIR $WORKPATH
RUN yum -y install vim net-tools wget
CMD /bin/bash

# 写完之后构建镜像
docker build -t my-centos:1.0 .
# 运行新镜像
docker run --name my-centos -it my-centos:1.0

运行结果如下:

image-20230912103614491

通过这两个例子,大部分的Dockerfile指令都使用过了。

应用的构建和发布

下面就用go语言写一个简单的web服务应用,然后将该应用构建成镜像并运行。

准备应用

创建一个go项目,创建一个main.go文件,写入如下内容:

package main

import (
	"github.com/gin-gonic/gin"
	"log"
	"net/http"
)

func main() {
	r := gin.Default()
	r.GET("/image_demo", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"code":    200,
			"message": "This is a go web service",
		})
	})
	err := r.Run("0.0.0.0:8080")
	if err != nil {
		log.Panicln(err)
	}
}

使用go build main.go编译go代码,这样就得到了一个main.exe的可执行文件。

镜像的构建

首先新建一个web目录,用于存放任意一个地方新建一个image_build的目录,在该目录下编写Dockerfile文件,并把刚刚得到的main文件移动到该目录下,并记得赋予main文件有 x 的权限(chmod +x main)。Dockerfile文件的内容如下:

# 使用官方的 Golang 镜像作为基础镜像
FROM golang:alpine
LABEL auth="bing" version="1.0" description="This is a go web service"
WORKDIR /web
COPY ./main /web
ENTRYPOINT ["/web/main"]
# 准备要暴露端口,也就是应用的默认端口
EXPOSE 8080

运行应用

image-20230912141825952

访问浏览器的结果如下:

image-20230912141948040

可以看到,虽然Dockerfile中准备要对外映射的端口是8080,但是我实际运行的时候使用了8888端口,没有使用8080端口。也就是说EXPOSE指令指定的端口只是告诉使用者该应用的默认端口是8080,仅此而已。

需要注意的地方

  1. 如果你是java程序,可以不用看,因为java代码在编译的时候中间有个JVM,屏蔽了底层的操作系统。

  2. 如果你是go程序,并且写代码的环境还是 windows 操作系统。那么需要注意这里要使用交叉编译的方式来编译main.go文件。也就是需要在windows操作系统下能够编译出可以在linux操作系统下跑的可执行文件。只需要修改GOOS这个环境变量即可。记得实验完成后换回windows。

    image-20230912144012355

build cache

测试环境搭建

以下面的Dockerfile文件为例,我简单讲一下镜像的构建过程和对build cache的理解。

FROM centos:7
LABEL auth="Tom"
COPY hello.log /var/log/
RUN yum -y install vim
CMD /bin/bash

Dockerfile文件写完之后,使用docker build -t test:1.0构建test:1.0镜像。在第一次构建镜像的过程中:

  1. 第一条 FROM 指令是 Dockerfile 中唯一不可缺少的指令,它为最终构建出的镜像设定了一个基础镜像。该语句并不会产生新的镜像层,它是使用指定的镜像(centos:7)作为基础镜像层的。由于每一个镜像由两部分组成,一个是该镜像的文件系统;另一个是记录该文件系统元数据的一个json文件,简称json镜像文件。docker daemon 根据进行镜像ID找到 centos:7 镜像,然后提取出对应的json文件,以备下一条指令镜像层构建时使用。
  2. LABEL 指令仅修改上一步中提取出的镜像 json 文件内容,在 json文件中添加 LABEL auth=“Tom”,无需更新镜像文件系统,但也会生成一个新的镜像层。这是因为新镜像层的ID就是把文件系统和json文件这两个部分的内容序列化为一个json串,docker daemon对这个json串进行哈希运算,得到一个 SHA256 哈希值,这个哈希值就是镜像ID。由于 json 文件发生了变化,导致 SHA256 哈希值的改变,镜像ID也就发生了变化,所以产生了新的镜像层。
  3. COPY 指令会将宿主机中的指定文件复制到容器中的指定目录,所以会改变该镜像层文件系统大小,并生成新的镜像层文件系统内容。所以 json 文件中的镜像 ID 也就发生了变化,产生了新的镜像层。
  4. RUN 指令本身并不会改变镜像层文件系统大小,但由于其 RUN 的命令是 yum install,而该命令运行的结果是下载并安装一个工具,所以导致 RUN 命令最终也改变了镜像层文件系统大小,所以也就生成了新的镜像层文件系统内容。所以 json 文件中的镜像 ID 也就发生了变化,产生了新的镜像层。
  5. 对于 CMD 或 ENTRYPOINT 指令,其是不会改变镜像层文件系统大小的,因为其不会在docker build 过程中执行。所以该条指令没有改变镜像层文件系统大小。但对于 CMD 或 ENTRYPOINT 指令,由于其是将来容器启动后要执行的命令,所以会将该条指令写入到 json 文件中,会引发 json 文件的变化。所以 json 文件中的镜像 ID 也就发生了变化,产生了新的镜像层。

修改Dockerfile文件

FROM centos:7
LABEL auth="Tom"
COPY hello.log /var/log/
RUN yum -y install vim
CMD /bin/bash
# 就是在最后添加一条指令
EXPOSE 9000

此时再构建新的镜像 test:2.0,会发现没有下载安装 vim 的过程了,并且发现了很多的 Using cache。说明这是使用了 build cache。然后使用docker history test:1.0和docker history test:2.0这两条命令查看它们每层的镜像ID,会发现test1.0和test:2.0的前五个镜像层的ID一模一样,这也说明了test:2.0的镜像在构建过程中复用了test:1.0的镜像层。

build cache 机制

Docker Daemnon 通过 Dockerfile 构建镜像时,当发现即将新构建出的镜像(层)与本地已存在的某镜像(层)重复时,默认会复用已存在镜像(层)而不是重新构建新的镜像(层),这种机制称为 docker build cache 机制。该机制不仅加快了镜像的构建过程,同时也大量节省了Docker 宿主机的空间。

Docker 构建缓存并不是存储在内存中,而是存储在 Docker 的本地存储中。这个本地存储通常位于 Docker 守护进程的数据目录下,例如在 Linux 系统上,这个目录通常是 /var/lib/docker。所以,无论是关闭 Docker 引擎,还是重启 Docker 宿主机,只要该镜像(层)存在于本地,那么就会复用。

build cache 失效

  1. Dockerfile 文件发生变化

    当 Dockerfile 文件中某个指令内容发生变化,那么从发生变化的这个指令层开始的所有镜像层 cache 全部失效。即从该指令行开始的镜像层将构建出新的镜像层,而不再使用 build cache,即使后面的指令并未发生变化。因为镜像关系本质上是一种树状关系,只要其上层节点变了,那么该发生变化节点的所有下层节点也就全部变化了。

  2. ADD 或 COPY 指令内容变化

    Dockerfile 文件内容没有变化,但 ADD 或 COPY 指令所复制的文件内容发生了变化,同样会使从该指令镜像层开始的后面所有镜像层的 build cache 失效。

  3. RUN 指令外部依赖变化

    与 ADD/COPY 指令相似。Dockerfile 文件内容没有变化,但 RUN 命令的外部依赖发生了变化,例如本例中要安装的 vim 软件源发生了变更(版本变化、下载地址变化等),那么从发生变化的这个指令层开始的所有镜像层 cache 全部失效。

  4. 指定不使用 build cache

    有些时候为了确保在镜像构建过程中使用到新的数据,在镜像构建 docker build 时,通过添加 --no-cache 选项指定不使用 build cache。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值