Docker(七)Dockerfile
Dockerfile是Docker用来构建镜像的文本文件,包含自定义的指令和格式。可以通过docker build命令从Dockerfile中构建镜像。这个过程与传统分布式集群的编排配置过程相似,且提供了一系列统一的资源配置语法。用户可以用这些统一的语法命令来根据需求进行配置,通过这份统一的配置文件,在不同的平台上进行分发,需要使用时就可以根据配置文件自动化构建同时,Dockerfile与镜像配合使用,使Docker在构建时可以充分利用镜像的功能进行缓存,大大提升了Docker的使用效率。
一、Dockerfile示例
1.1 下载指定版本的镜像
如果不加版本默认下载的镜像是latest版本。如果我们不想pull最新版本的镜像,想下载其他版本的镜像呢。这里以centos为例,centos现在下载的是centos7.4的,比如我想下载centos7.2的base镜像,tag应该是什么呢?
官方镜像库:https://hub.docker.com/
根据tags信息,我们来下载一下centos7.2版本的镜像:
# docker pull centos:7.2.1511
# docker images centos #上面pull完毕之后,查看一下本地的镜像,下面的记过可以看到多了镜像可以了。
REPOSITORY TAG IMAGE ID CREATED SIZE docker.io/centos 7.2.1511 0a2bad7da9b5 3 weeks ago 194.6 MB docker.io/centos latest d123f4e55e12 3 weeks ago 196.6 MB
1.2 编写一个简单的dockerfile文件并执行
# cat /docker/dockerfile/base/Dockerfile
FROM docker.io/centos:7.2.1511 MAINTAINER The Base CentOS7.2 Project <chaishao@51niux.com> RUN yum install -y net-tools gcc gcc-c++ \ && yum install -y lrzsz openssh-server openssh-clients CMD /bin/bash
# docker build -t basecentos:7.2 /docker/dockerfile/base/ #创建一个basecentos的镜像,tag是7.2,然后去/docker/dockerfile/base/目录下找Dockerfile
# docker images basecentos
REPOSITORY TAG IMAGE ID CREATED SIZE basecentos 7.2 8ae9ed4083ee 9 minutes ago 378.4 MB
#利用我们新搞的镜像创建一个容器,登录并测试一下,可以看到官网镜像没有的软件包,现在已经存在了。
1.3 编写一个简单的nginx安装的dockerfile
构建Dockerfile上下文
# pwd #下面nginx镜像制作的dockerfile所在的目录
/docker/dockerfile/nginx
# tree
. ├── Dockerfile #Dockerfile文件 └── nginx-1.12.1.tar.gz #官网下载的nginx安装包
Dockerfile文件内容
# cat /docker/dockerfile/nginx/Dockerfile
FROM basecentos:7.2 #从本地的basecentos:7.2镜像构建 MAINTAINER chaishao "chaishao@51niux.com" #维护者的信息 COPY ./nginx-1.12.1.tar.gz /opt/ #将本地的nginx安装包拷贝到镜像的/opt目录下 RUN yum install -y openssl* libjpeg libjpeg-devel libpng libpng-devel freetype freetype-devel libxml2 libxml2-devel \ zlib zlib-devel ncurses ncurses-devel curl curl-devel gd gd2 gd-devel gd2-devel pcre pcre-devel RUN cd /opt/ && tar zxf nginx-1.12.1.tar.gz && cd nginx-1.12.1 && \ ./configure --prefix=/usr/local/nginx --with-http_stub_status_module --with-http_ssl_module && make && make install RUN echo "The page is test docker nginx page." > /usr/share/nginx/html/index.html #替换下nginx的index.html首页 EXPOSE 80 #暴露80端口 CMD ["/usr/local/nginx/sbin/nginx"] #容器启动后启动nginx服务
构建镜像
# docker build -t="nginx:1.12" /docker/dockerfile/nginx #指定镜像的名称为nginx,标签是1.12,用/docker/dockerfile/nginx目录下的dockerfile构建镜像,下面是一大波构建过程
Sending build context to Docker daemon 984.6 kB Step 1 : FROM basecentos:7.2 ---> 8ae9ed4083ee Step 2 : MAINTAINER chaishao "chaishao@51niux.com" ---> Running in e7c5336d2605 ---> ba6516a5b899 Removing intermediate container e7c5336d2605 Step 3 : COPY ./nginx-1.12.1.tar.gz /opt/ ---> 8af9ee38c259 Removing intermediate container 631d58677522 Step 4 : RUN yum install -y openssl* libjpeg libjpeg-devel libpng libpng-devel freetype freetype-devel libxml2 libxml2-devel zlib zlib-devel ncurses ncurses-devel curl curl-devel gd gd2 gd-devel gd2-devel pcre pcre-devel ---> Running in 63e28d0abf5e Loaded plugins: fastestmirror Loading mirror speeds from cached hostfile * base: mirrors.aliyun.com * extras: mirrors.aliyun.com * updates: mirrors.aliyun.com Package 1:openssl-libs-1.0.2k-8.el7.x86_64 already installed and latest version No package gd2 available. No package gd2-devel available. Resolving Dependencies --> Running transaction check ---> Package curl.x86_64 0:7.29.0-25.el7.centos will be updated #开始yum了,yum过程就不粘贴了 ...... Complete! #yum结束了 ---> 35235c64d25d Removing intermediate container 63e28d0abf5e Step 5 : RUN cd /opt/ && tar zxf nginx-1.12.1.tar.gz && cd nginx-1.12.1 && ./configure --prefix=/usr/local/nginx --with-http_stub_status_module --with-http_ssl_module && make && make install ---> Running in 42c780e33d25 #开始编译了 checking for OS + Linux 3.10.0-327.el7.x86_64 x86_64 checking for C compiler ... found + using GNU C compiler + gcc version: 4.8.5 20150623 (Red Hat 4.8.5-16) (GCC) checking for gcc -pipe switch ... found ...... make[1]: Leaving directory `/opt/nginx-1.12.1' ---> aad8f93194ac Removing intermediate container 6355f2a001f7 Step 6 : RUN echo "The page is test docker nginx page." > /usr/local/nginx/html/index.html ---> Running in faf2f3695aff ---> fd09abdd5904 Removing intermediate container faf2f3695aff Step 7 : EXPOSE 80 ---> Running in aed0db3782a6 ---> 3b629eb0e918 Removing intermediate container aed0db3782a6 Step 8 : CMD /usr/local/nginx/sbin/nginx ---> Running in a06429ed7c2d ---> 4aea74adfca1 Removing intermediate container a06429ed7c2d Successfully built 4aea74adfca1 #这里表示构建成功,并产生了镜像的id 4aea74adfca1
注:去除构建缓存的两种方式:
# docker build -t="nginx:1.12" /docker/dockerfile/nginx --no-cache
在dockerfile的MAINTAINER下面加上ENVREFRESH_DATE 2017-11-26
查看镜像
# docker images nginx #可以看到镜像id跟上面是一致的
REPOSITORY TAG IMAGE ID CREATED SIZE nginx 1.12 4aea74adfca1 2 minutes ago 604.9 MB
启动容器并查看
# docker run -it -d -p 8080:80 --name nginx-server-01 nginx:1.12 /bin/bash
[root@slave06 ~]# docker attach nginx-server-01
[root@f32fe591d7c4 /]# /usr/local/nginx/sbin/nginx
三、Dockerfile的使用解释
3.1 docker build命令和镜像构建过程
https://blog.51niux.com/?id=185 #记录了镜像的命令参数和build参数详解。
通过上面的链接知道其docker build命令其参数有3中类型(PATH、-、URL),表示构建上下文(context)的3种来源。这里的构建上下文 (简称上下文)是指传入docker build命令的所有文件。这种情况下,将本地主机的一个包含Dockerfile的目录中的所有内容作为上下文。上下文通过docker build命令传入到Docker daemon后,便开始按照Dockerfile中的内容构建镜像。
Dockerfile描述了组装镜像的步骤,其中每条指令都是单独执行的。除了FROM指令,其他每一条指令都会在上一条指令所生成镜像的基础上执行,执行完后会生成一个新的镜像层,新的镜像层覆盖在原来的镜像之上从而形成了新的镜像。Dockerfile所生成的最终镜像就是在其基础镜像上面叠加一层层的镜像层组建的。
为了提高镜像构建的速度,Docker daemon会缓存构建过程中的中间镜像。当从一个已在缓存中的基础镜像开始构建新镜像时,会将Dockerfile中的下一条指令和基础镜像的所有子镜像做一个比较,如果有一个子镜像是由相同的指令生成的,则命中缓存,直接使用该镜像,而不用再生成一个新的镜像。在寻找缓存的过程中,COPY和ADD指令与其他指令稍有不同,其他指令只对比生成镜像的指令字符串是否相同;ADD和COPY指令除了对比指令字符串,还要对比容器中的文件内容和ADD、COPY所添加的文件内容是否相同。此外,镜像构建过程中,一旦缓存失败,则后续的指令都将生成新的镜像,而不再使用缓存。
3.2 Dockerfile指令
Dockerfile的基本格式如下:INSTRUCTION arguments
在Dockerfile中,指令(INSTRUCTION)不区分大小写,但是为了与参数区分,推荐大写。Docker会顺序执行Dockerfile中的指令,第一条指令必须是FROM指令, 它用于指定构建镜像的基础镜像。
在Dockerfile中以#开头的行是注释,而在其他位置出现的#会被当成参数。
Dockerfile中的指令有FROM、MAINTAINER、RUN、CMD、EXPOSE、ENV、ADD、COPY、ENTRYPOINT、VOLUME、USER、WORKDIR、ONBUILD,错误的指令会被忽略。
Dockerfile 分为四部分:基础镜像信息、维护者信息、镜像操作指令和容器启动时执行指令。
基础镜像信息 # FROM 维护者信息 # MAINTAINER 镜像操作指令 # RUN、COPY、ADD、EXPOSE等 容器启动时执行指令 #CMD、ENTRYPOINT
ENV
格式:ENV <key> <value>或ENV <key>=<value> ...
ENV指令可以为镜像创建出来的容器生命环境变量。并且在Dockerfile中,ENV指令声明的环境变量会被后面的特定指令(即ENV、ADD、COPY、WORKDIR、EXPOSE、VOLUME、USER)解释使用。其他指令使用环境变量时,使用格式为$variable_name或者${variable_name}。在变量前面添加斜杠\可以转义,如\$foo或者\${foo},将会被分别转换为$foo和${foo},而不是环境变量所保存的值。另外,ONBUILD指令不支持环境替换。
FROM
格式:FROM <image> 或FROM <image>:<tag>
FROM指令的功能是为后面的指令提供基础镜像,因此一个有效的Dockerfile必须以FROM指令作为第一条非注释指令。从公共镜像库中拉取镜像很容器,基础镜像可以选择任何有效的镜像。在一个Dockerfile中,FROM指令可以出现多次,这样会构建多个镜像。在每个镜像创建完成后,Docker命令行界面会输出该镜像的ID。若FROM指令中参数tag为空,则tag默认是latest;若参数image或tag指定的镜像不存在,则返回错误。
COPY
格式:COPY <src> <dest>
COPY指令复制<src>所指向的文件或目录,将它添加到新镜像中,复制的文件或目录在镜像中的路径是<dest>。<src>所指定的源可以有多个,但必须在上下文中,即必须是上下文根目录的相对路径。不能使用形如COPY ../something /something这样的指令。此外,<src>可以使用通配符指向所有匹配通配符的文件或目录,例如,COPY hom* /mydir/ 表示添加所有以“hom”开头的文件到目录/mydir/中。
<dest>可以使文件或目录,但必须是目标镜像中的绝对路径或者相对于WORKDIR的相对路径(WORKDIR即Dockerfile中WORKDIR指令指定的路径,用来为其他指令设置工作目录)。若<dest>以反斜杠/结尾则其指向的是目录;否则指向文件。<src>同理。若<dest>是一个文件,则<src>的内容会被写入到<dest>中;否则<src>所指向的文件或目录中的内容会被复制添加到<dest>目录中。当<src>指定多个源时,<dest>必须是目录。另外,如果<dest>不存在,则路径中不存在的目录会被创建。
ADD
格式:ADD <src> <dest>
ADD与COPY指令在功能上很相似,都支持复制本地文件到镜像的功能,但ADD指令还支持其他功能。<src>可以是一个指向一个网络文件的URL,此时若<dest>指向一个目录,则URL必须是完全路径,这样可以获得该网络文件的文件名filename,该文件会被复制添加到<dest>/<filename>。例如,ADD http:example.com/foobar /会创建文件/foobar。
<src>还可以指向一个本地压缩归档文件,该文件在复制到容器中时会被解压提取,如ADD example.tar.xz /。但若URL中的文件为归档文件则不会被解压提取。
ADD和COPY指令虽然功能相似,当一般推荐使用COPY,因为COPY只支持本地文件,相比ADD而言,它更透明。
RUN
RUN指令有两种格式:
RUN <commadn> (shell格式) RUN ["executable","param1","param2" ](exec格式,推荐格式)
RUN指令会在前一条命名创建出的镜像的基础上创建一个容器,并在容器中运行命令,在命令结束运行后提交容器为新镜像,新镜像被Dockerfile中的下一条指令使用。
RUN指令的两种格式表示命令在容器中的两种运行方式。当使用shell格式时,命令通过/bin/sh -c运行;当使用exec格式时,命令是直接运行的,容器不调用shell程序,即容器中没有shell程序。exec格式中的参数会当成JSON数据被Docker解析,故必须使用双引号而不能使用单引号。因为exec格式不会再shell中执行,所以环境变量的参数不会被替换,例如,当执行CMD ["echo","$HOME"]指令时,$HOME不会做变量替换。如果希望运行shell程序,指令可以写成CMD["shell","-c",“echo",“$HOME”]
CMD
CMD指令有3种格式:
CMD <command> (shell格式) CMD ["executable","param1","param2"] (exec格式,推荐格式) CMD ["param1","param2"] (为ENTRYPOINT指令提供参数)
CMD指令提供容器运行时的默认值,这些默认值可以是一条指令,也可以是一些参数。一个Dockerfile中可以有多条CMD指令,但只有最后一条CMD指令有效。CMD ["param1","param2"] 格式是在CMD指令和ENTRYPOINT指令配合时使用的,CMD指令中的参数会添加到ENTRYPOINT指令中。使用shell的exec格式时,命令在容器中的运行方式与RUN指令相同。不同在于,RUN指令在构建镜像时执行命令,并生成新的镜像;CMD指令在构建镜像时并不执行任何命令,而是在容器启动时默认将CMD指令作为第一条执行的命令。如果用户在命令行界面运行docker run命令时指定了命令参数,则会覆盖CMD指令中的命令。
ENTRYPOINT
ENTRYPOINT指令有两种格式:
ENTRYPOINT <command> (shell格式) ENTRYPOINT ["executable","param1","param2"] (exec格式,推荐格式)
ENTRPOINT指令和CMD指令类似,都可以让容器在每次启动时执行相同的命令,但它们之间又有不同。一个Dockerfile中可以有多条ENTRYPOINT指令,但只有最后一条ENTRYPOINT指令有效。当使用shell格式时,ENTRYPOINT指令会忽略任何CMD指令和docker run命令的参数,并且会运行在/bin/bash -c中。这意味着ENTRPOINT指令进程为/bin/sh -c的子进程,进程在容器中的PID将不是1,且不能接受Unix信号。即当使用docker stop <container>命令时,命令进程接受不到SIGTERM信号。推荐使用exec格式,使用此格式时,docker run传入的命令参数会覆盖CMD指令的内容并且附加到ENTRYPOINT指令的参数中。从ENTRYPOINT的使用中可以看出,CMD可以使参数,也可以是指令,而ENTRYPOINT只能是命令;另外,docker run命令提供的运行命令参数可以覆盖CMD,但不能覆盖ENTRYPOINT。
USER
USER格式为:USER daemon
指定运行容器时的用户名或UID,后续的RUN也会使用指定用户。
当服务不需要管理员权限时,可以通过该命令指定运行用户。并且可以在之前创建所需要的用户,例如:RUN groupadd -r postgress && useradd -r -g postgres postgres。要临时获取管理员权限可以使用gosu,而不推荐sudo。
WORKDIR
格式为WORKDIR /path/to/workdir
为后续的 RUN、CMD、ENTRYPOINT 指令配置工作目录。 可以使用多个 WORKDIR 指令,后续命令如果参数是相对路径,则会基于之前命令指定的路径。例如 WORKDIR /a WORKDIR b WORKDIR c RUN pwd 则最终路径为 /a/b/c。
ARG
ARG是Docker1.9 版本才新加入的指令。格式:ARG<name>[=<default value>]
ARG指定了一个变量在docker build的时候使用,可以使用--build-arg <varname>=<value>来指定参数的值,不过如果构建的时候不指定就会报错。
LABEL
LABEL执行是向镜像中添加元数据,一个LABEL是一个键值对,在LABEL的值中使用添加空格,使用双引号和反斜线。示例:
LABEL "com.example.vendor"="ACME Incorporated" LABEL com.example.label-with-value="foo" LABEL version="1.0" LABEL description="This text illustrates \ that label-values can span multiple lines."
Docker可以指定多个标签,每个标签产生一个新层,多个层会导致效率低下,因此建议将多个标签合并到一个标签:
LABEL multi.label1="value1" multi.label2="value2" other="value3"
或:
LABEL multi.label1="value1" \ multi.label2="value2" \ other="value3"
查看镜像的LABEL可以使用docker inspect命令:
"Labels": { "com.example.vendor": "ACME Incorporated" "com.example.label-with-value": "foo", "version": "1.0", "description": "This text illustrates that label-values can span multiple lines.", "multi.label1": "value1", "multi.label2": "value2", " other": "value3" },
VOLUME
格式:VOLUME ["/data"]
可以将本地文件夹或者其他container的文件夹挂载到container中。
ONBUILD
格式:ONBUILD [INSTRUCTION]
ONBUILD指令的功能是添加一个将来执行的触发器指令到镜像中。当该镜像作为FROM指令的参数时,这些触发器指令就会在FROM指令执行时加入到构建过程中。尽管任何指令都可以注册成一个触发器指令,但ONBUILD指令中不能包含ONBUILD指令,并且不会触发FROM和MAINTAINER指令。当需要制作一个基础镜像来构建其他镜像时,它可能需要将应用源代码加入到一个指定目录中,还可能需要执行一个构建脚本。此时不能仅仅调用ADD和RUN指令,因为现在还不能访问应用源代码,并且不同应用的源代码是不同的。不能简单地提供一个Dockerfile模板给应用开发者,它与特定应用代码耦合,会引发低效、易错、难以更新等问题。这些场景的解决方案是使用ONBUILD指令注册触发器指令,利用ONBUILD指令构建一个语言栈镜像,该镜像可以构建任何用该语言编写的用户软件的镜像。
ONBUILD指令的具体执行步骤如下:
在构建过程中,ONBUILD指令会添加到触发器指令镜像元数据中。这些触发器指令不会再当前构建过程中执行。
在构建过程最后,触发器指令会被存储在镜像详情中,其主键是OnBuild,可以使用docker inspect命令查看。
之后该镜像可能作为其他Dockerfile中FROM指令的参数。在构建过程中,FROM指令会寻找ONBUILD触发器指令,并且会以它们注册的顺序执行。若有触发器指令执行失败,则FROM指令被中止,并会返回失败;若所有触发器指令执行成功,则FROM指令完成并继续执行下面的指令。在镜像构建完成后,触发器指令会被清除,不会被子孙镜像继承。
使用包含ONBUILD指令的Dockerfile构建的镜像应该有特殊的标签,如ruby:2.0-onbuild。在ONBUILD指令中添加ADD或COPY指令时要额外注意。假如新构建过程的上下文缺失了被添加的资源,那么新构建过程会失败。给ONBUILD镜像添加标签,可以提示编写Dockerfile的人员小心应对。
STOPSIGNAL
给系统发送退出信号(终止信号),该信号可以是与内核系统调用 “9”相似,或者是格式为SIGNAME的信号名称,例如SIGKILL。STOPSIGNAL signal
HEALTHCHECK
HEALTHCHECK指令是在Docker 1.12版本加入,此指令有两种格式:
HEALTHCHECK [OPTIONS] CMD command #在容器内运行一个命令检测容器健康 HEALTHCHECK NONE #禁止从基础镜像集成任何健康检测
HEALTHCHECK 指令告诉docker如何去检测容器仍然在工作,例如你的web容器因为一个无限循环卡住无法创建新的连接它可以告诉容器不再健康,即使现在进程依然在运行。
容器的初始状态是‘starting’,如果健康检测容器正常他的状态会是'healthy'状态码为“0”, 如果健康检测出现连续失败会出现'unhealthy' 状态码为“1”,如果因为非健康执行退出则为“reserved”状态码为“2”。
参数介绍:
可以出现在CMD的参数: --interval=DURATION #默认间隔30s --timeout=DURATION #默认超时时间30s --retries=N #默认重试3次 HEALTHCHECK --interval=5m --timeout=3s \CMD curl -f http://localhost/|| exit 1 #上面三个参数连贯起来就是,健康检测默认间隔5分钟运行一次,如果超过3秒则认为超时,如果尝试三次一直失败则认为容器不再健康执行"exit 1"。 #假如说在Dockerfile里面多次出现这个参数,将会采用最后一个HEALTHCHECK 指令,这个是和CMD指令一样的。如果你想获取容器现在的状态可以使用“docker inspect [CONTAINER ID ]”获取当前容器的health_status
3.3 Dockerfile实践心得
在构建Dockerfile文件时,如果遵守这些实践方式,可以高效地使用Docker。
使用标签
给镜像打上标签,易读的镜像标签可以帮助了解镜像的功能,如docker build -t=“ruby:2.0-onbuild”。
谨慎选择基础镜像
选择基础镜像时,尽量选择当前官方镜像库中的镜像。不同镜像的大小不同,目前Linux镜像大小有如下关系:
busybox < debian < centos <ubuntu
同时在构建自己的Docker镜像时,只安装和更新必须使用的包。FROM指令应该包含参数tag。
充分利用缓存
Docker daemon会顺序执行Dockerfile中的指令,而且一旦缓存失效,后续命令将不能使用缓存。为了有效的利用缓存,需要保证指令的连续性,尽量将所有Dockerfile文件中相同的部分都放在前面,而将不同的部分放在后面。
正确使用ADD和COPY指令
尽管ADD和COPY用法和作用很相近,但COPY仍是首选。COPY相对于ADD而言,功能简单够用。COPY仅提供本地文件向容器的基本复制功能。ADD有额外的一些功能,比如支持复制本地压缩包(复制到容器中会自动压缩)和URL远程资源。因此,ADD比较符合逻辑的使用方式是ADD roots.tar.gz / 。
当在Dockerfile中的不同部分需要用到不同的文件时,不要一次性地将这些文件都添加到镜像中去, 而是在需要时逐个添加,这样也有利用于充分利用缓存。另外,考虑到镜像大小的问题,使用ADD指令去获取远程URL中的压缩包不是推荐的做法。应该使用RUN wget或RUN curl代替。这样可以删除解压后不再需要的文件,并且不需要在镜像中再添加一层。
另外,尽量使用docker volume共享文件,而不是使用ADD或COPY指令添加文件到镜像中。
RUN指令
为了使Dockerfile易读、易理解和可维护,在使用比较长的RUN指令时可以使用反斜杠\分割多行。大部分使用RUN指令的场景是运行yum命令,在该场景下请注意如下几点:
不要在一行中单独使用指令RUN yum update。当软件源更新后,这样做会引起缓存问题,导致RUN yum install指令运行失败。所以,RUN yum update和RUN yum install应该写在同一行,如RUN yum update && yum install -y package-bar
避免使用指令RUN yum upgrade和RUN yum upgrade。因为在一个无特权的容器中,一些必要的包会更新失败。如果需要更新一个包(如foo),直接使用指令RUN yum install -y foo。
在Docker的核心概念中,提交镜像是廉价的,镜像之间有层级关系,像一棵树。不要害怕镜像的层数过多。可以在任意层创建一个容器。因此,不要将所有的命令写在一个RUN指令中。RUN指令分层符合Docker的核心概念,这很像源码控制。
CMD和ENTRYPOINT指令
CMD和ENTRYPOINT指令指定了容器运行的默认命令,推荐两者结合使用。使用exec格式的ENTRYPOINT指令设置固定的默认命令和参数,然后使用CMD指令设置可变的参数。
不要在Dockerfile中做端口映射
Docker的两个核心概念是可重复性和可移植性,镜像应该可以在任意主机上运行多次。使用Dockerfile的EXPOSE指令,虽然可以将容器端口映射到主机端口上,但会破坏Docker的可移植性,且这样的镜像在一台主机上只能移动一个容器。所以端口映射应在docker run命令中用-p参数指定。
使用Dockerfile共享Docker镜像
若要共享镜像,只需共享Dockerfile文件即可。共享Dockerfile文件具有以下优点:
Dockerfile文件可以加入版本控制,这样可以追踪文件的变化和回滚错误。 通过Dockerfile文件,可以清楚镜像构建的过程。 使用Dockerfile文件构建的镜像具有确定性。