欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

docker——6、Dockerfile

程序员文章站 2024-03-12 08:34:08
...
Dockerfile  -->docker build(RUN) --> images --> docker run(CMD) --> 运行容器

Docker中有个非常重要的概念叫做——镜像(Image)。Docker 镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变。
镜像的定制实际上就是定制每一层所添加的配置、文件。如果我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本,用这个脚本来构建、定制镜像,那么之前提及的无法重复的问题、镜像构建透明性的问题、体积的问题就都会解决。这个脚本就是 Dockerfile。
Dockerfile 是一个文本文件,其内包含了一条条的指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。

  • 编写.dockerignore文件
  • 容器只运行单个应用
  • 将多个RUN指令合并为一个
  • 基础镜像的标签不要用latest
  • 每个RUN指令后删除多余文件
  • 选择合适的基础镜像(alpine版本最好)
  • 设置WORKDIR和CMD
  • 使用ENTRYPOINT (可选)
  • 在entrypoint脚本中使用exec
  • COPY与ADD优先使用前者
  • 合理调整COPY与RUN的顺序
  • 设置默认的环境变量,映射端口和数据卷
  • 使用LABEL设置镜像元数据
  • 添加HEALTHCHECK

Dockerfile语法说明

1、FROM: 指定基础镜像

FROM <repository>[:<tag>]
FROM <repository>@<digest> 

<repository>指定作为base image的名称
<tag>:可省, base image的标签,为可选项,省略时默认为latest
定制镜像的时候都是以一个镜像为基础,在这个基础上面进行定制。FROM在Dockerfile中是必须的指令,而且必须是第一条指令。
1)在Docker Hub上有非常多的官方镜像,比如服务类(nginx/redis)、语言类(node/openjdk/python)、操作系统类(ubuntu/debian/centos)等,我们可以直接拿来使用。
2)除了选择现有的镜像作为基础镜像外,Docker还存在一个特殊的镜像,名为scratch。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。

2、MAINTAINER

MAINTAINER <author's detail>

<author's detail>可以是任何文本信息,但约定使用作者名称及邮件地址
如: MAINTAINER "can<[email protected]>"
声明作者信息,可以放在文件任何位置,建议放在FROM后面

3、LABEL = = = …
标签,将后面的元数据添加到镜像中,可以用docker inspect查看

4、COPY:复制文件
用于从Docker主机复制文件至创建的新映射文件。COPY 指令将从构建上下文目录中<源路径>的文件复制到新的一层的镜像内的<目标路径>位置。
和 RUN 指令一样,也有两种格式,一种类似于命令行,一种类似于函数调用:

COPY <src>... <dest>
COPY ["<src>",... "<dest>"]

<src>:要复制的源文件或目录,支持使用通配符
<dest>:目标路径,即正在创建image的文件系统路径;建议为使用绝对路径,否则COPY指定则以WORKDIR为其其实路径;
注意:在路径中有空白字符时,通常使用第2种格式
文件复制准则:
(1)必须是build上下文中的路径,不能是其父目录中的文件
(2)如果是目录,则其内部文件或子目录会被递归复制,但目录自身不会被复制
(3)如果指定了多个,或在中使用了通配符,则必须是一个目录,且必须以/结尾
(4)如果事先不存在,它将会被自动创建,这包括其父目录路径
示例:

[[email protected] img1]# vim /img1/Dockerfile
# Description: test image
FROM busybox:latest
MAINTAINER "Can <[email protected]>"
#LABEL maintainer="Can <[email protected]>"
COPY index.html /data/web/html/
[[email protected] img1]# vim /img1/index.html
Busybox httpd server.
[[email protected] img1]# docker build -t tinyhttpd:v0.1-1 ./
Sending build context to Docker daemon  3.072kB
Step 1/3 : FROM busybox:latest
latest: Pulling from library/busybox
57c14dd66db0: Pull complete 
Digest: sha256:7964ad52e396a6e045c39b5a44438424ac52e12e4d5a25d94895f2058cb863a0
Status: Downloaded newer image for busybox:latest
 ---> 3a093384ac30
Step 2/3 : MAINTAINER "Can <[email protected]>"
 ---> Running in 9915777d4c1f
Removing intermediate container 9915777d4c1f
 ---> 65f3bb45add2
Step 3/3 : COPY index.html /data/web/html/
 ---> d91b191fe38f
Successfully built d91b191fe38f
Successfully tagged tinyhttpd:v0.1-1
[[email protected] img1]# docker image ls  #查看镜像创建成功
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
tinyhttpd           v0.1-1              d91b191fe38f        2 minutes ago       1.2MB
[[email protected] img1]# docker run --name tinyweb1 --rm tinyhttpd:v0.1-1 cat /data/web/html/index.html
Busybox httpd server.

5、ADD

ADD <src>... <dest>
ADD ["<src>",... "<dest>"]

ADD指令类似于COPY指令,ADD支持使用TAR文件和URL路径
这个命令将会把src(源)目录、文件、远程文件URL复制到镜像的文件系统中,存放目录为dest(目标)目录。如果src是压缩文件会帮你解压出来。
操作准则:
(1)如果<src>为URL且<dest>不以/结尾,到指定的文件将被下载并直接被创建为<dest>;如果<dest>以/结尾,则文件名URL指定的文件将被直接下载并保存为<dest>/<filename>
(2)如果<src>是一个本地系统上的压缩格式的tar文件,它将被展开为一个目录,其行为类似于“tar -x”,命令;然而,通过URL获取到的tar文件将不会自动展开
(3)如果<src>有多个,或其间接或直接使用了通配符,则<dest>必须是一个以/结尾的目录路径;如果<dest>不以/结尾,则其被视作一个普通文件,<src>的内容将被直接写入到<dest>;
例如:
将URL的tar包下载至/use/local/src/

#ADD http://nginx.org/download/nginx-1.15.8.tar.gz /use/local/src/  

将本地tar包解压至/use/local/src/

ADD nginx-1.15.8.tar.gz /usr/local/src/

6、WORKDIR: 指定工作目录

WORKDIR <dirpath>

用于为Dockerfile中所有的RUN CMD ENTRYPOINT COPY ADD指定设定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录,如该目录不存在,WORKDIR 会帮你建立目录。
说明:
在Dockerfile文件中,WORKDIR指令可以出现多次,其路径也可以为相对路径,不过,其实相对比前一个WORKDIR指令指定的路径。另外,WORKDIR也可调用由ENV指定定义的变量
例如:

WORKDIR /var/log
WORKDIR $STATEPATH

7、VOLUME
用于在image中创建一个挂载点目录,以挂载Docker host上的卷或其他容器上的卷

VOLUME <mountpoint>
VOLUME ["<mountpoint>"]

如果挂载点目录路径下此前在文件存在,docker run命令会在卷挂载完成后将此前的所有文件复制到新挂载的卷中
如:VOLUME /data/mysql/ 将容器挂载到/data/mysql/

8、EXPOSE: 声明端口
用于为容器打开指定要监听的端口以实现与外部通信

 EXPOSE <port1>[/<protocol>][<port1>[/<protocol>]...]

<protocol>用于指定传输层协议,可为tcp或udp二者之一,默认为TCP协议
EXPOSE指令可一次指定多个端口,例如:EXPOSE 11211/udp 11211/tcp
EXPOSE指令是声明运行时容器提供服务端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务。在Dockerfile中写入这样的声明有两个好处,一个是帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;另一个用处则是在运行时使用随机端口映射时,也就是docker run -P时,会自动随机映射EXPOSE的端口。
要将EXPOSE和在运行时使用-p <宿主端口>:<容器端口>区分开来。-p,是映射宿主端口和容器端口,换句话说,就是将容器的对应端口服务公开给外界访问,而EXPOSE仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行端口映射。

[[email protected] img1]# vim Dockerfile
EXPOSE 80/tcp
[[email protected] img1]# docker build -t tinyhttpd:v0.1-6 ./
[[email protected] img1]# docker run --name tinyweb1 --rm tinyhttpd:v0.1-6 /bin/httpd -f -h /data/web/html
[[email protected] ~]# docker inspect tinyweb1
[[email protected] ~]# curl 10.0.0.2
Busybox httpd server.

[[email protected] img1]# docker run --name tinyweb1 --rm -P tinyhttpd:v0.1-6 /bin/httpd -f -h /data/web/html
[[email protected] img1]# docker port tinyweb1
80/tcp -> 0.0.0.0:32768

9、ENV: 设置环境变量
用于为奖项定义所需的环境变量,并可被Dockerfile文件中位于其后的其他指令(如ENV、 ADD、COPY等所调用),后面的其它指令,如RUN,还是运行时的应用,都可以直接使用这里定义的环境变量。
调用格式为$variable_name${variable_name}
格式有两种:

ENV <key> <value>
ENV <key1>=<value1> <key2>=<value2>...

第一种格式中,<key>之后的所有内容均会被视作其<value>的组成部分,因此,一次只能设置一个变量;
第二种格式可用一次设置多个变量,每个变量为一个“<key>=<value>”的键值对,如果<value>中包含空格,可以以反斜线()进行转义,也可通过对<value>加引号进行标识;另外,反斜线也可用于续行;
定义多个变量时,建议使用第二种格式,以便在同一层中完成所有功能

ENV VERSION=1.0 DEBUG=on \
    NAME="Happy Feet"

这个例子中演示了如何换行,以及对含有空格的值用双引号括起来的办法,这和 Shell 下的行为是一致的。

[[email protected] img1]# docker run --name tinyweb1 --rm -P tinyhttpd:v0.1-7 printenv
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=0ba2f53395ea
DOC_ROOT=/data/web/html/
WEB_SERVER_PACKAGE=nginx-1.15.8
HOME=/root
[[email protected] img1]# docker run --name tinyweb1 --rm -P -e WEB_SERVER_PACKAGE="nginx-1.15.2" tinyhttpd:v0.1-7 printenv
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=b77090575ca9
WEB_SERVER_PACKAGE=nginx-1.15.2
DOC_ROOT=/data/web/html/
HOME=/root

10、RUN: 执行命令
用于指定docker build过程中运行的程序,其可以是任何命令。run指令在定制镜像时是最常用的指令之一。
shell 格式:CMD <command>
exec 格式:CMD ["executable", "param1", "param2"...]
(1)第一个格式中,<command>通常是一个shell命令,且以"/bin/sh -c“来运行它,这意味着此进程在容器中的PID不为1,不能接收Unix信号,因此,当使用docker stop <container>命令停止容器时,此进程接收不到SIGTERM信号;就像直接在命令行中输入的命令一样,如RUN echo 'hello, world!' >
(2)第二种语法格式中的参数是一个JSON参数的数组,其中<executable>为要运行的命令,后面的<paramN>为传递给命令的选项或参数;然而,此种格式指定的命令不会以”/bin/sh -c"来发起,因此常见的shell操作如变量替换以及通配符(?,*等)替换将不会进行;不过,如果要运行的命令依赖于此shell特性的话,可以将其替换为类似下面的格式。

RUN ["/bin/bash","-c","<executable>","<param1>"]

类似于函数调用,将可执行文件和参数分开,如RUN [ "sh", "-c", "echo $HOME" ]

Dockerfile中每一个指令都会建立一层,RUN也不例外。每一个RUN的行为,就和刚才我们手工建立镜像的过程一样:新建立一层,在其上执行这些命令,执行结束后,commit这一层的修改,构成新的镜像。所以我们在使用的时候尽可能将指令进行整合(可以使用&&将各个所需命令串联起来)。

注意:json数组中,要使用双引号

11、CMD:容器启动命令
和 RUN 相似,可用于运行任何命令或应用程序,不过,二者的运行时间点不同
(1)RUN指令运行与映像文件构建过程中,而CMD指令运行于基于Dockerfile构建出的新映像文件启动一个容器时。
(2)CMD指令的首要目的在于为启动的容器指定默认要运行的程序,且其运行结束后,容器也将终止;不过,CMD指定的命令其可以被docker run的命令行选项所覆盖。
(3)在Dockerfile中可以存在多个CMD命令,但仅最后一个会生效
shell 格式:CMD <命令>
exec 格式:CMD ["可执行文件", "参数1", "参数2"...]
在指定了 ENTRYPOINT 指令后,用 CMD 指定具体的参数。
参数列表格式:CMD ["参数1", "参数2"...]
#用于为ENTRYPOINT指令提供默认参数

12、USER
用于指定运行image时的或运行Dockerfile中任何RUN、CMD或ENTRYPOINT指令指定的程序时的用户名或UID
默认情况下,container的运行身份为root用户

USER <UID> | <UserName>

需要注意的是,可以为任意数字,但实践中其必须为/etc/passwd中某用户的有效UID,否则,docker run 命令将运行失败
示例一:

[[email protected] ~]# cd img1
[[email protected] img1]# vim Dockerfile 
# Description: test image
FROM busybox:latest
MAINTAINER "Can <[email protected]>" #作者说明
#LABEL maintainer="Can <[email protected]>"  #同上,写法不一样

ENV DOC_ROOT /data/web/html/ #环境变量
ENV WEB_SERVER_PACKAGE="nginx-1.15.8.tar.gz" 

COPY index.html ${DOC_ROOT:-/data/web/html/} #复制index.html到/data/web/html/

#COPY index.html /data/web/html/
COPY yum.repos.d /etc/yum.repos.d/

ADD http://nginx.org/download/${WEB_SERVER_PACKAGE} /usr/local/src/ 
#下载URL上的tar包并解压至/usr/local/src
#ADD nginx-1.15.8.tar.gz /usr/local/src/ #将本地的tar包放至/usr/local/src

WORKDIR /usr/local/   #指定工作目录
#ADD ${WEB_SERVER_PACKAGE} ./src/  #将tar包解压至/usr/local/src

VOLUME /data/mysql/  #将容器挂载到/data/mysql/

EXPOSE 80/tcp  #为容器打开指定要监听的端口80

#执行命令

RUN cd /usr/local/src && \
    tar xf ${WEB_SERVER_PACKAGE}
[[email protected] img1]# docker build -t tinyhttpd:v0.1-9 ./
[[email protected] img1]# docker run --name tinyweb1 --rm -P -e WEB_SERVER_PACKAGE="nginx-1.15.8" -it tinyhttpd:v0.1-9
/usr/local # cd src
/usr/local/src # ls
nginx-1.15.8         nginx-1.15.8.tar.gz

示例二:

[[email protected] ~]# cd img2
[[email protected] img2]# vim Dockerfile 
FROM busybox
LABEL maintainer="Can <[email protected]>" app="httpd"
ENV WEB_DOC_ROOT="/data/web/html"

RUN mkdir -p $WEB_DOC_ROOT && \
    echo '<h1>Busybox httpd server.</h1>' > ${WEB_DOC_ROOT}/index.html

CMD /bin/httpd -f -h ${WEB_DOC_ROOT}
#CMD ["/bin/sh","-c","/bin/httpd","-f","-h /data/web/html"]
[[email protected] img1]# docker image inspect tinyhttpd:v0.2-1
 "Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                "WEB_DOC_ROOT=/data/web/html"
        ],
 "Cmd": [
                "/bin/sh",
                "-c",
                "/bin/httpd -f -h ${WEB_DOC_ROOT}"
        ],
[[email protected] img1]# docker run --name tinyweb2 -it --rm -P tinyhttpd:v0.2-1
[[email protected] img2]# docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED              STATUS              PORTS               NAMES
fcc58e45aa6f        tinyhttpd:v0.2-1    "/bin/sh -c '/bin/ht…"   About a minute ago   Up About a minute                       tinyweb2
[[email protected] img2]# docker exec -it tinyweb2 /bin/sh
/ # ps
PID   USER     TIME  COMMAND
    1 root      0:00 /bin/httpd -f -h /data/web/html
    6 root      0:00 /bin/sh
   10 root      0:00 ps
/ # printenv
WEB_DOC_ROOT=/data/web/html
HOSTNAME=fcc58e45aa6f
SHLVL=1
HOME=/root
TERM=xterm
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PWD=/

示例三:

[[email protected] img3]# vim entrypoint.sh
#!/bin/sh
# 
cat > /etc/nginx/conf.d/www.conf << EOF
server {
    server_name $HOSTNAME;
    listen ${IP:-0.0.0.0}:${PORT:-80};
    root ${NGX_DOC_ROOT:-/usr/share/nginx/html};
}
EOF

exec "[email protected]"
[[email protected] img3]# chmod +x entrypoint.sh
[[email protected] img3]# vim Dockerfile
FROM nginx:1.14-alpine
LABEL maintainer="Can <[email protected]>"

ENV NGX_DOC_ROOT='/data/web/html/'

ADD index.html ${NGX_DOC_ROOT}
ADD entrypoint.sh /bin/

CMD ["/usr/sbin/nginx","-g","daemon off;"]

ENTRYPOINT ["/bin/entrypoint.sh"]

[[email protected] img3]# vim index.html
New Doc Root for Nginx
[[email protected] img3]# docker build -t myweb:v0.3-6 ./
[[email protected] ~]# docker run --name myweb1 --rm -P myweb:v0.3-6
Sending build context to Docker daemon  4.096kB
Step 1/7 : FROM nginx:1.14-alpine
 ---> c5b6f731fbc0
Step 2/7 : LABEL maintainer="Can <[email protected]>"
 ---> Using cache
 ---> 84f5b7412dea
Step 3/7 : ENV NGX_DOC_ROOT='/data/web/html/'
 ---> Using cache
 ---> b27e4dc2fdd6
Step 4/7 : ADD index.html ${NGX_DOC_ROOT}
 ---> Using cache
 ---> 10730b8eabb1
Step 5/7 : ADD entrypoint.sh /bin/
 ---> Using cache
 ---> 738dbbfd7980
Step 6/7 : CMD ["/usr/sbin/nginx","-g","daemon off;"]
 ---> Running in 45863d9cb43a
Removing intermediate container 45863d9cb43a
 ---> a46abb57ac5f
Step 7/7 : ENTRYPOINT ["/bin/entrypoint.sh"]
 ---> Running in 1d484ccac2c8
Removing intermediate container 1d484ccac2c8
 ---> 1900fce136a2
Successfully built 1900fce136a2
Successfully tagged myweb:v0.3-6
[[email protected] img3]# docker exec -it myweb1 /bin/sh
/ # cat /etc/nginx/conf.d/www.conf 
server {
    server_name b97459d32aaa;
    listen 0.0.0.0:80;
    root /data/web/html/;
}
/ # netstat -tnl
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      
/ # wget -O - -q localhost #访问测试
/ # wget -O - -q b97459d32aaa
New Doc Root for Nginx

#测试植入参数

[[email protected] ~]# docker kill myweb1
[[email protected] ~]# docker run --name myweb1 --rm -P -e "PORT=8080" myweb:v0.3-6
[[email protected] img3]# docker exec -it myweb1 /bin/sh
/ # netstat -tnl
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       
tcp        0      0 0.0.0.0:8080            0.0.0.0:*               LISTEN      
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN 

典型案例参考:https://github.com/docker-library/mysql/
官方文档:https://docs.docker.com/engine/reference/builder/
最佳实践文档:https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/

13、Dockerfile HEALTHCHECK
Dockerfile中使用HEALTHCHECK的形式有两种:
1、HEALTHCHECK [options] CMD command(本次详细解释)
2、HEALTHCHECK NODE 意思是禁止从父镜像继承的HEALTHCHECK生效
下面我们主要介绍第一种形式的应用:
options有三个参数可设定:
interval:间隔(s秒、m分钟、h小时),从容器运行起来开始计时interval秒(或者分钟小时)进行第一次健康检查,随后每间隔interval秒进行一次健康检查;还有一种特例请看timeout解析。
timeout:执行command需要时间,比如curl 一个地址,如果超过timeout秒则认为超时是错误的状态,此时每次健康检查的时间是timeout+interval秒。
retries:连续检查retries次,如果结果都是失败状态,则认为这个容器是unhealth
CMD关键字后面可以跟执行shell脚本的命令或者exec数组。CMD后面的命令执行完的返回值代表容器的运行状况,可能的值:0 health状态,1 unhealth状态,2 reserved状态,这个没细研究,用的也很少。
注意:在Dockerfile中只能有一个HEALTHCHECK指令。如果您列出多个,则只有最后一个HEALTHCHECK将生效。

[[email protected] img3]# vim Dockerfile
FROM nginx:1.14-alpine
LABEL maintainer="Can <[email protected]>"

ENV NGX_DOC_ROOT='/data/web/html/'

ADD index.html ${NGX_DOC_ROOT}
ADD entrypoint.sh /bin/

EXPOSE 80/tcp

HEALTHCHECK --start-period=3s CMD wget -O - -q http://${IP:-0.0.0.0}:${PORT:-80}/

CMD ["/usr/sbin/nginx","-g","daemon off;"]

ENTRYPOINT ["/bin/entrypoint.sh"]
[[email protected] ~]# docker run --name myweb1 --rm -P -e "PORT=8080" myweb:v0.3-8
127.0.0.1 - - [27/Jan/2019:06:36:24 +0000] "GET / HTTP/1.1" 200 23 "-" "Wget" "-"
127.0.0.1 - - [27/Jan/2019:06:36:55 +0000] "GET / HTTP/1.1" 200 23 "-" "Wget" "-"
127.0.0.1 - - [27/Jan/2019:06:37:25 +0000] "GET / HTTP/1.1" 200 23 "-" "Wget" "-"
127.0.0.1 - - [27/Jan/2019:06:37:56 +0000] "GET / HTTP/1.1" 200 23 "-" "Wget" "-"
127.0.0.1 - - [27/Jan/2019:06:38:26 +0000] "GET / HTTP/1.1" 200 23 "-" "Wget" "-"
127.0.0.1 - - [27/Jan/2019:06:38:57 +0000] "GET / HTTP/1.1" 200 23 "-" "Wget" "-"
127.0.0.1 - - [27/Jan/2019:06:39:27 +0000] "GET / HTTP/1.1" 200 23 "-" "Wget" "-"

14、SHELL
SHELL指令可以覆盖命令的shell模式所使用的默认shell。Linux的默认shell是[“/bin/sh”, “-c”],Windows的是[“cmd”, “/S”, “/C”]。SHELL指令必须以JSON格式编写。
SHELL指令在有两个常用的且不太相同的本地shell:cmdpowershell,以及可选的sh的windows上特别有用。
SHELL指令可以出现多次。每个SHELL指令覆盖之前的SHELL指令设置的shell,并影响随便的指令。

15、ARG
只在build中使用。该默认值可以在构建命令 docker build 中用 --build-arg <参数名>=<值> 来覆盖。
在 1.13 之前的版本,要求 --build-arg 中的参数名,必须在 Dockerfile 中用 ARG 定义过了,换句话说,就是 --build-arg 指定的参数,必须在 Dockerfile 中使用了。如果对应参数没有被使用,则会报错退出构建。从 1.13 开始,这种严格的限制被放开,不再报错退出,而是显示警告信息,并继续构建。这对于使用 CI 系统,用同样的构建流程构建不同的 Dockerfile 的时候比较有帮助,避免构建命令必须根据每个 Dockerfile 的内容修改。