目录
Dockerfile文件
Dockerfile 是用于构建 Docker 镜像的脚本文件,由一系列指令构成。通过 docker build命令构建镜像时,Dockerfile 中的指令会由上到下依次执行,每条指令都将会构建出一个镜像。这就是镜像的分层。因此,指令越多,层次就越多,创建的镜像就越多,效率就越低。所以在定义 Dockerfile 时,能在一个指令完成的动作就不要分为两条。
Dockerfile 指令
-
FROM
【语法】FROM <image>[:<tag>]
【解析】用于指定基础镜像,且必须是第一条指令;若省略了 tag,则默认为 latest。
-
MAINTAINER
【语法】MAINTAINER <name>
【解析】MAINTAINER 指令的参数填写的一般是维护者姓名和信箱。不过,该指令官方已不建议使用,而是使用 LABEL 指令代替。
-
LABEL
【语法】LABEL <key>=<value> <key>=<value>
【解析】LABEL 指令中可以以键值对的方式包含任意镜像的元数据信息,用于替代MAINTAINER 指令。通过 docker inspect 可查看到 LABEL 与 MAINTAINER 的内容。
-
ENV
【语法 1】ENV <key> <value>
【解析】用于指定环境变量,这些环境变量,后续可以被 RUN 指令使用,容器运行起来之后,也可以在容器中获取这些环境变量。
【语法 2】ENV <key1>=<value1> <key2>=<value2> …
【解析】可以设置多个变量,每个变量为一对<key>=<value>指定。
-
WORKDIR
【语法】WORKDIR path
【解析】容器打开后默认进入的目录,一般在后续的 RUN、CMD、ENTRYPOINT、ADD 等指令中会引用该目录。可以设置多个 WORKDIR 指令。后续 WORKDIR 指令若用的是相对路径,则会基于之前 WORKDIR 指令指定的路径。在使用 docker run 运行容器时,可以通过-w 参数覆盖构建时所设置的工作目录。
-
RUN
【语法 1】RUN <command>
【解析】这里的<command>就是 shell 命令。docker build 执行过程中,会使用 shell 运行指定的 command。
【语法 2】RUN [ " EXECUTABLE " , " PARAM1 " , " PARAM2 " , …]
【解析】在 docker build 执行过程中,会调用第一个参数"EXECUTABLE"指定的应用程序运行,并使用后面第二、三等参数作为应用程序的运行参数。
-
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。
-
ENTRYPOINT
【语法 1】ENTRYPOINT [ " EXECUTABLE " , " PARAM1 " , " PARAM2 " , …]
【解析】在容器启动过程中,即在执行 docker run 时,会调用执行"EXECUTABLE"指定的应用程序,并使用后面第二、三等参数作为应用程序的运行参数。
【语法 2】ENTRYPOINT command param1 param2, …
【解析】这里的 command 就是 shell 命令。在容器启动过程中,即在执行 docker run 时,会运行指定的 shell 命令。
-
EXPOSE
【语法】EXPOSE <port> [<port>…]
【解析】指定容器准备对外暴露的端口号,但该端口号并不会真正的对外暴露。若要真正暴露,则需要在执行 docker run 命令时使用-p(小 p)来指定说要真正暴露出的端口号。
-
ARG
【语法】ARG < varname >[=<default value>]
【解析】定义一个变量,该变量将会使用于镜像构建运行时。若要定义多个变量,则需要定义多个 ARG 指令。
-
ADD
【语法 1】ADD <src> <dest>
【语法 2】ADD [" <src>" , " <dest>"] # 路径中存在空格时使用双引号引起来
【解析】该指令将复制当前宿主机中指定文件 src 到容器中的指定目录 dest 中。src 可以是宿主机中的绝对路径,也可以时相对路径。但相对路径是相对于 docker build 命令所指定的路径的。src 指定的文件可以是一个压缩文件,压缩文件复制到容器后会自动解压为目录;src 也可以是一个 URL,此时的 ADD 指令相当于 wget 命令;src 最好不要是目录,其会将该目录中所有内容复制到容器的指定目录中。dest 是一个绝对路径,其最后面的路径必须要加上斜杠,否则系统会将最后的目录名称当做是文件名的。
-
COPY
【说明】功能与 ADD 指令相同,只不过 src 不能是 URL。若 src 为压缩文件,复制到容器后不会自动解压。
-
ONBUILD
【语法】ONBUILD [INSTRUCTION]
【解析】该指令用于指定当前镜像的子镜像进行构建时要执行的指令。
-
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 文件。
编写Dockerfile文件
在 hello_world 目录中新建一个 Dockerfile 文件,切记文件名必须是Dockerfile。内容如下:
# 表示基础镜像是scratch,也就是一个空白的镜像。
FROM scratch
# 将当前目录下的hello文件添加到镜像的根目录(/)下。
ADD hello /
# 设置了容器的默认命令,也就是当运行一个基于这个镜像的容器时,如果不指定运行的命令,那么默认会运行/hello这个命令。
CMD ["/hello"]
构建镜像
![image-20230909200714875](https://img-blog.csdnimg.cn/img_convert/90060f89351ccad7e2ac147aa679e1bb.png)
-
-t 用于指定要生成的镜像的<repository>与<tag>。若省略 tag,则默认为 latest。
-
最后的点(.)是一个
宿主机的 URL 路径
,构建镜像时会从该路径中查找 Dockerfile 文件
。同时,在 Dockerfile 中 ADD、COPY 指令中有使用的是相对路径,那个相对路径就相对的是这个路径。不过需要注意,即使 ADD、COPY 指令中使用绝对路径来指定源文件,该源文件所在路径也必须要在这个 URL 指定目录或子目录内,否则将无法找到该文件。
运行新镜像
构建自己的 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
运行结果如下:
通过这两个例子,大部分的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](https://img-blog.csdnimg.cn/img_convert/7a5b3d726a5c8907bcfb806b0e33ed2a.png)
访问浏览器的结果如下:
![image-20230912141948040](https://img-blog.csdnimg.cn/img_convert/e2062d94faffe6a003f77e7bf162a178.png)
可以看到,虽然Dockerfile中准备要对外映射的端口是8080,但是我实际运行的时候使用了8888端口,没有使用8080端口。也就是说EXPOSE指令指定的端口只是告诉使用者该应用的默认端口是8080,仅此而已。
需要注意的地方
:
如果你是java程序,可以不用看,因为java代码在编译的时候中间有个JVM,屏蔽了底层的操作系统。
如果你是go程序,并且写代码的环境还是 windows 操作系统。那么需要注意这里要使用
交叉编译
的方式来编译main.go文件。也就是需要在windows操作系统下能够编译出可以在linux操作系统下跑的可执行文件。只需要修改GOOS这个环境变量即可。记得实验完成后换回windows。
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镜像。在第一次构建镜像的过程中:
- 第一条 FROM 指令是 Dockerfile 中唯一不可缺少的指令,它为最终构建出的镜像设定了一个基础镜像。该语句并不会产生新的镜像层,它是使用指定的镜像(centos:7)作为基础镜像层的。由于每一个镜像由两部分组成,一个是该镜像的文件系统;另一个是记录该文件系统元数据的一个json文件,简称json镜像文件。docker daemon 根据进行镜像ID找到 centos:7 镜像,然后提取出对应的json文件,以备下一条指令镜像层构建时使用。
- LABEL 指令仅修改上一步中提取出的镜像 json 文件内容,在 json文件中添加 LABEL auth=“Tom”,无需更新镜像文件系统,但也会生成一个新的镜像层。这是因为新镜像层的ID就是把文件系统和json文件这两个部分的内容序列化为一个json串,docker daemon对这个json串进行哈希运算,得到一个 SHA256 哈希值,这个哈希值就是镜像ID。由于 json 文件发生了变化,导致 SHA256 哈希值的改变,镜像ID也就发生了变化,所以产生了新的镜像层。
- COPY 指令会将宿主机中的指定文件复制到容器中的指定目录,所以会改变该镜像层文件系统大小,并生成新的镜像层文件系统内容。所以 json 文件中的镜像 ID 也就发生了变化,产生了新的镜像层。
- RUN 指令本身并不会改变镜像层文件系统大小,但由于其 RUN 的命令是 yum install,而该命令运行的结果是下载并安装一个工具,所以导致 RUN 命令最终也改变了镜像层文件系统大小,所以也就生成了新的镜像层文件系统内容。所以 json 文件中的镜像 ID 也就发生了变化,产生了新的镜像层。
- 对于 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 失效
-
Dockerfile 文件发生变化
当 Dockerfile 文件中某个指令内容发生变化,那么从发生变化的这个指令层开始的所有镜像层 cache 全部失效。即从该指令行开始的镜像层将构建出新的镜像层,而不再使用 build cache,即使后面的指令并未发生变化。因为镜像关系本质上是一种树状关系,只要其上层节点变了,那么该发生变化节点的所有下层节点也就全部变化了。
-
ADD 或 COPY 指令内容变化
Dockerfile 文件内容没有变化,但 ADD 或 COPY 指令所复制的文件内容发生了变化,同样会使从该指令镜像层开始的后面所有镜像层的 build cache 失效。
-
RUN 指令外部依赖变化
与 ADD/COPY 指令相似。Dockerfile 文件内容没有变化,但 RUN 命令的外部依赖发生了变化,例如本例中要安装的 vim 软件源发生了变更(版本变化、下载地址变化等),那么从发生变化的这个指令层开始的所有镜像层 cache 全部失效。
-
指定不使用 build cache
有些时候为了确保在镜像构建过程中使用到新的数据,在镜像构建 docker build 时,通过添加 --no-cache 选项指定不使用 build cache。