Dockerfile 详解

标签:Docker首次发布:2024-01-19最近修改:2026-08-19

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 文件创建在这里。文件内容如下:

c
#include <stdio.h>int main(){    printf("hello docker world!\n");    return 0;}

使用 gcc 编译 hello.c 文件。

bash
[root@centos hello_world]# gcc --static hello.c -o hello[root@centos hello_world]# ./hellohello docker world!

编写 Dockerfile 文件

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

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

构建镜像

bash
[root@centos hello_world]# docker build -t hello-docker-world .[+] Building 0.1s (5/5) FINISHED                                                              docker:default => [internal] load build definition from Dockerfile                                                    0.0s => => transferring dockerfile: 449B                                                                    0.0s => [internal] load .dockerignore                                                                       0.0s => => transferring context: 2B                                                                         0.0s => [internal] load build context                                                                       0.0s => => transferring context: 866.59kB                                                                   0.0s => [1/1] ADD hello /                                                                                   0.0s => exporting to image                                                                                  0.0s => => exporting layers                                                                                 0.0s => => writing image sha256:ee010c254a9a531932a4fa12252138a4a19e93bcb3b84d96847b5dfe6263aef6            0.0s => => naming to docker.io/library/hello-docker-world                                                   0.0s[root@centos hello_world]# docker imagesREPOSITORY           TAG       IMAGE ID       CREATED          SIZEhello-docker-world   latest    ee010c254a9a   43 seconds ago   866kB
  • -t 用于指定要生成的镜像的<repository>与<tag>。若省略 tag,则默认为 latest。

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

运行新镜像

bash
[root@centos hello_world]# docker run --name myHelloWorld hello-docker-worldhello docker world!

构建自己的 centos 镜像

dockerfile
# 新建一个Dockerfile文件# vim Dockerfile# 在文件中写入如下内容:FROM centos:7MAINTAINER bing 1234@qq.comLABEL auth="bing" version="1.0" description="This is a Centos image I wrote myself"ENV WORKPATH /usr/localWORKDIR $WORKPATHRUN yum -y install vim net-tools wgetCMD /bin/bash# 写完之后构建镜像docker build -t my-centos:1.0 .# 运行新镜像docker run --name my-centos -it my-centos:1.0

运行结果如下:

bash
[root@centos my_centos]# docker run --name my-centos -it my-centos:1.0[root@c6bc2e6250e6 local]# ifconfigeth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500        inet 172.17.0.2  netmask 255.255.0.0  broadcast 172.17.255.255        ether 02:42:ac:11:00:02  txqueuelen 0  (Ethernet)

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

应用的构建和发布

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

准备应用

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

go
package mainimport (    "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 文件的内容如下:

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

运行应用

bash
# 构建镜像[root@centos image_build]# docker build -t image_build .[+] Building 15.7s (8/8) FINISHED => [internal] load .dockerignore => => transferring context: 2B => [internal] load build definition from Dockerfile => => transferring dockerfile: 363B => [internal] load metadata for docker.io/library/golang:alpine......# 运行镜像[root@centos image_build]# docker run --name image_build -dp 8888:8080 image_buildec954787595c4de12f07a7f36e6a1675820a9982eac3ee167db49461724027b0

接口测试结果如下:

bash
[root@centos image_build]# curl http://192.168.11.10:8888/image_demo{  "code": 200,  "message": "This is a go web service"}

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

注意
  1. 如果是 java 程序,可以不用看,因为 java 代码在编译的时候中间有个 JVM,屏蔽了底层的操作系统。
  2. 如果是 go 程序,并且写代码的环境还是 windows 操作系统。那么需要注意这里要使用交叉编译的方式来编译 main.go 文件。也就是需要在 windows 操作系统下能够编译出可以在 linux 操作系统下跑的可执行文件。只需要修改 GOOS 这个环境变量即可。
    powershell
    # 设置目标操作系统为 Linux$env:GOOS = "linux"# 设置目标架构为 amd64$env:GOARCH = "amd64"

build cache

测试环境搭建

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

dockerfile
FROM centos:7LABEL auth="Tom"COPY hello.log /var/log/RUN yum -y install vimCMD /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 文件

dockerfile
FROM centos:7LABEL auth="Tom"COPY hello.log /var/log/RUN yum -y install vimCMD /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。

Comments

评论区将在滚动到这里时加载。