本文接着上一篇文章《Dockerfile 参考手册(一)》接续Dockerfile相关的学习。本文主要介绍Dockerfile中最重要的部分,构建镜像的主角:指令。
文档是基于Docker v17.09 版本。
文章内容完全是翻译官方文档。
01 Dockerfile中常用的指令
这里总结Dockerfile中常用指令的用法。
01.1 FROM
FROM <image> [AS <name>]
或者
FROM <image>[:<tag>] [AS <name>]
或者
FROM <image>[@<digest>] [AS <name>]
FROM
指令初始化一个新的编译阶段,为后续的指令设置一个基础镜像,由于这个原因,一个有效的Dockerfile
文件必须以FROM
指令开头,这个镜像可以是任何有效的镜像,从公共仓库拉取一个镜像作为开始比较简单。
- 在
Dockerfile
文件中,ARG
指令是唯一一个可以放在FROM
之前的指令。参见Understand how ARG and FROM interact。 - 在一个
Dockerfile
文件中,FROM
指令可以出现多次,用于创建多个镜像或者使用一个作为另一个编译阶段的依赖。不过,每一个新的FROM
指令之前都会输出一个提交之后的最新的镜像ID。每一个FROM
指令都会清楚之前指令创建的任何状态。 -
tag
或者digest
的值是可选的,如果你省略它们,编译器都会认为是默认的标签latest
,如果编译器没有发现这个tag
的值,就会返回一个错误。
理解ARG和FROM是如何相互作用的
在第一个FROM
指令之前,ARG
指令声明的任何变量,FROM
指令都是支持的。
ARG CODE_VERSION=latest
FROM base:${CODE_VERSION}
CMD /code/run-app
FROM extras:${CODE_VERSION}
CMD /code/run-extras
FROM
指令之前的ARG
声明是处于编译阶段之外的,因此它是不能被用于FROM
指令之后的任何指令的,在第一个FROM
指令之前的ARG
指令声明了默认值,在编译阶段内的ARG
指令没有设值:
ARG VERSION=latest
FROM busybox:$VERSION
ARG VERSION
RUN echo $VERSION > image_version
01.2 RUN
RUN
指令有两种形式:
-
RUN <command>
(shell的形式,命令运行在shell中,在Linux上默认的是/bin/sh -c
,在windows上默认的是cmd /S /C
) -
RUN ["executable", "param1", "param2"]
(exec的形式)
RUN
指令是在当前镜像的最上面的一个新层上执行任何命令的,然后提交结果,提交结果之后的镜像被用于Dockerfile
中下面步骤的操作基础。
RUN
指令的分层性和提交即生成镜像符合Docker的核心理念,其中提交是廉价的,在一个镜像历史的任何一个点都是可以创建容器的,这很像源代码的管理。
exec的形式可以避免shell字符串写死,可以传参数,RUN
命令还可以指定执行基础镜像中不包含的可执行shell。
默认的shell形式可能被SHELL
指令修改。
在shell形式中,你可以使用反斜杠\
,在下一行继续编写同一条RUN指令,例如下面两行:
RUN /bin/bash -c 'source $HOME/.bashrc; \
echo $HOME'
它们和下面这一行是同样的作业:
RUN /bin/bash -c 'source $HOME/.bashrc; echo $HOME'
注意:为了使用不同shell,而不使用'/bin/sh',使用exec形式转化你渴望使用的shell,例如
RUN ["/bin/bash", "-c", "echo hello"]
注意:exec形式会被解析为JSON数组,这意味着,你必须使用双引号(")包围单词,而不是使用单引号(')。
注意:不像shell形式那样,exec形式不能调用一个shell命令,这意味着不能做正常的shell处理过程,例如,
RUN [ "echo", "$HOME" ]
在$HOME
上不会做变量替换。如果你想使用shell处理,要么使用shell的形式,要么直接执行一个shell,例如:RUN [ "sh", "-c", "echo $HOME" ]
。当使用exec形式直接执行一个shell命令的时候,这和shell形式是一样的,它是一个正在做环境变量扩展的shell,而不是docker。
注意:在JSON格式中,反斜杠转义是非常必要的,尤其是在windows环境下,反斜杠是路径分隔符。下面一行将会被认为是shell的形式处理,而不是一个有效的JSON,然后就意想不到的失败了:RUN ["c:\windows\system32\tasklist.exe"]
,正确的语法是这样的:RUN ["c:\\windows\\system32\\tasklist.exe"]
。
RUN
指令的缓存在下一次编译的时候不会自动失效,一个像这样的指令RUN apt-get dist-upgrade -y
在下一次的编译中会重用。RUN
指令的缓存可以使用--no-cache
标识置失效,例如,docker build --no-cache
。
更多信息参见Dockerfile
的最佳实践指导。
RUN
指令的缓存也可能被ADD
指令置为失效,详细参照下面的介绍。
已知问题[RUN]
Issue 783,当使用AUFS文件系统时可能会遇到文件权限问题。例如,在你尝试
rm
一个文件的时候会注意到这个问题。对于一个有最新aufs版本(可能会设置
dirperm1
的挂载操作)的系统来说,docker可能会尝试自动修复这个问题,使用的方法是,挂载一个有dirperm1
操作的层。关于dirperm1
操作的更多详细信息可以在aufs
的 man page上找到。
如果你的系统不支持dirperm1
,这个问题描述了一个变通的方法。
01.3 CMD
CMD
指令有三种形式:
-
CMD ["executable","param1","param2"]
(exec形式,一个首选的形式) -
CMD ["param1","param2"]
(作为ENTRYPOINT的默认参数) -
CMD command param1 param2
(shell形式)
在一个Dockerfile
文件中只能有一个CMD
指令,如果你使用多个CMD
,只有最后一个是生效的。
CMD
命令的主要目的是为运行时的容器提供一些默认参数和命令,这些默认包括一些可执行的文件,或者可以省略可执行文件,这种情况下你必须同时指定一个ENTRYPOINT
指令。
注意:如果
CMD
指令用于为ENTRYPOINT
指令指定默认参数,CMD
和ENTRYPOINT
指令必须都是JSON数组的格式。
注意:exec形式是被作为JSON数组解析的,这意味着你必须使用双引号("),而不是单引号(')。
注意:不像shell形式那样,exec形式不能调用一个shell命令,这意味着不能做正常的shell处理过程,例如,
RUN [ "echo", "$HOME" ]
在$HOME
上不会做变量替换。如果你想使用shell处理,要么使用shell的形式,要么直接执行一个shell,例如:RUN [ "sh", "-c", "echo $HOME" ]
。当使用exec形式直接执行一个shell命令的时候,这和shell形式是一样的,它是一个正在做环境变量扩展的shell,而不是docker。
无论使用的是shell还是exec形式,CMD
指令这是的命令都是在运行这个镜像的时候被执行。
如果你使用shell形式的CMD
指令,<command>
将使用/bin/sh -c
执行:
FROM ubuntu
CMD echo "This is a test." | wc -
如果你想运行<command>
,而不是shell,你必须将命令表示成一个JSON数组,而且还要给出命令的全路径。这个数组形式的是CMD
的优先格式。任何额外的参数都必须单独表示为数组中的字符串:
FROM ubuntu
CMD ["/usr/bin/wc","--help"]
如果你想每次你的容器都执行同一个命令,你应该考虑将CMD
指令结合ENTRYPOINT
使用。详见ENTRYPOINT。
如果用户给docker run
指定参数,它将覆盖CMD
指定的默认命令。
注意:不要混淆
RUN
和CMD
,RUN
是实际的执行一个命令,并提交执行结果;CMD
指令在编译阶段不会执行任何命令,但是为镜像预制了一个命令。
01.4 LABEL
LABEL <key>=<value> <key>=<value> <key>=<value> ...
LABEL
指令为一个镜像添加一些元数据,一个LABEL
是一个键值对。为了能在LABEL
的value中包含空格,需要使用双引号,使用反斜杠可以被当做一个命令行解析。下面是一些用法:
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."
一个镜像可以有多个label,为了指定多个label,Docker建议尽可能地将所有的label合并到一个LABEL指令中,如果你使用很多label,每个LABEL
指令都能产生一个新的层,这回导致镜像是非常低效的,下面这个例子是产生一个镜像层的例子:
LABEL multi.label1="value1" multi.label2="value2" other="value3"
上面的也可以被写成:
LABEL multi.label1="value1" \
multi.label2="value2" \
other="value3"
Label是叠加的,它会包含FROM
的镜像中的Label
,如果Docker遇到一个已经存在的Label的key值,新的值会覆盖掉前面同一个key的所有Label值。
查看一个镜像的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"
},
01.5 MAINTAINER (过时的)
MAINTAINER <name>
MAINTAINER
指令是用来设置制作镜像的作者的,LABEL
指令实现这个功能更灵活,你最好使用它来替换MAINTAINER
,并且它可以设置任何你需要的元数据,并且容易查看,例如命令docker inspect
。你可以这样使用label来对应MAINTAINER
字段:
LABEL maintainer="SvenDowideit@home.org.au"
这样和其它的label一样是可以使用docker inspect
来查看的。
01.6 EXPOSE
EXPOSE <port> [<port>/<protocol>...]
EXPOSE
指令通知Docker容器在运行时监听指定的网络端口,你还可以指定监听的端口是TCP还是UDP,如果不指定协议,默认的是TCP。
EXPOSE
指令不会真正发布这个端口,它的作用是在构建镜像和运行容器的用户之间建立一个文档记录,指定哪个端口是被预定发布的。为了能够在运行容器的时候实际发布一个端口,在docker run
命令后使用-p
标识发布和映射一个或者多个端口,或者使用-p
标识发布所有的暴露端口并映射它们到高位端口。
设置端口直接指向主机系统的方法见 -P 标识的使用方法。docker network
命令支持创建容器之间的网络通信,并不需要发布和暴露指定端口,这是因为通过网络连接的容器,可以使用任何端口互相通信。更多的详细信息参见网络总结。
01.7 ENV
ENV <key> <value>
ENV <key>=<value> ...
ENV
指令设置环境变量<key>
的值<value>
,这个值将存在于Dockerfile
的所有后代中,也可能被内联替换(replaced inline)。
ENV
指令有两种形式,第一种形式是,ENV <key> <value>
,它会给一个变量设置一个值,第一个空格之后的整个字符串都将被视作<value>
——包含空格和双引号字符。
第二种形式是,ENV <key>=<value> ...
,允许一次性设置多个变量,注意,第二种形式在语法上使用了等号,而第一种形式中没有。像一般的命令行解析一样,双引号和反斜杠都可以用在value中包含空格的情况下。
例如:
ENV myName="John Doe" myDog=Rex\ The\ Dog \
myCat=fluffy
和
ENV myName John Doe
ENV myDog Rex The Dog
ENV myCat fluffy
它们在最后的镜像中产生相同的结果,但是推荐使用第一种形式,它只产生一个缓存层。
当运行那个产生的镜像的容器的时候,使用ENV
设置的环境变量将会被持久化。你可以使用docker inspect
查看这些值,使用docker run --env <key>=<value>
可以改变它们。
注意:环境变量的持久化可能会导致意外的副作用,例如设置了
ENV DEBIAN_FRONTEND noninteractive
,可能会混淆基于Debian的镜像的 apt-get 用户。为了给单个命令设置一个环境变量,可以使用RUN <key>=<value> <command>
。
01.8 ADD
ADD指令同样有两种形式:
ADD <src>... <dest>
-
ADD ["<src>",... "<dest>"]
(如果路径中包含空格,这种形式是必须的)
ADD
指令是从<src>
中copy文件、目录或者远程文件URL中的文件,然后添加它们到镜像的文件系统中,路径是<dest>
。
可以指定多个<src>
,但是它们如果是文件或者目录的话,必须是编译(编译上下文)原路径的相对路径。
每一个<src>
都可以包含通配符,匹配方式使用的是Go语言的文件路径的匹配规则 filepath.Match,例如:
ADD hom* /mydir/ # adds all files starting with "hom"
ADD hom?.txt /mydir/ # ? is replaced with any single character, e.g., "home.txt"
<dest>
是一个绝对路径,或者是WORKDIR
的相对路径,source中指定的文件会被copy到目的的容器中。
ADD test relativeDir/ # adds "test" to `WORKDIR`/relativeDir/
ADD test /absoluteDir/ # adds "test" to /absoluteDir/
当添加的文件或者目录的路径中包含特殊字符(例如[
和]
)时,你需要根据Go语言的规则对它们进行转义,防止它们被当作是通配符,例如,添加文件名为arr[0].txt
的文件。使用下面的方式:
ADD arr[[]0].txt /mydir/ # copy a file named "arr[0].txt" to /mydir/
所有新的文件和目录在创建的时候都必须有一个0的UID和GID。
在<src>
是一个远程文件URL的情形下,目的地必须有600的权限。如果检索到的远程文件有一个HTTP的Last-Modified
头信息,头信息的时间戳会被设置到目的文件的mtime
。然而,ADD
就像其他的任何文件处理过程一样,mtime
不包括在文件是否已经改变,缓存是否被更新。
注意:如果你通过标准输入传递一个
Dockerfile
文件(docker build - < somefile
),这时是没有编译上下文的,因此Dockerfile
中的ADD
指令仅能包含一个URL。你也可以通过标准输入传递一个压缩包(docker build - < archive.tar.gz
),Dockerfile
在压缩包的根目录,压缩包的其余部分作为编译的上下文。
注意:如果你的URL和文件是有权限保护的,你需要使用
RUN wget
,RUN curl
或者使用容器内的其他工具,因为ADD
指令不支持授权。
注意:如果
<src>
的内容已经改变的话,第一个遇到的ADD
指令将会使下面来自Dockerfile
的所有指令的缓存失效,这也包括RUN
指令的缓存。更多信息参见Dockerfile的最佳实践指导 。
ADD
遵循下列规则:
<src>
的路径必须在编译的上下文中,你不能使用ADD ../something /something
,因为docker build
的第一步是发送目录上下文(包括子目录)到docker的守护进程。如果
<src>
是一个URL,<dest>
不以斜杠结尾,那么从URL上下载的文件会被copy到<dest>
。如果
<src>
是一个URL,<dest>
以斜杠结尾,那么URL上指定的文件名就是下载的文件名<dest>/<filename>
,例如,ADD http://example.com/foobar /
将会创建那个文件/foobar
。URL必须是一个简单的路径,以便能够发现一个合适的文件名(http://example.com
是无效的)。如果
<src>
是一个目录,目录下的所有内容都会被copy,包括文件系统的元数据。
注意:文件目录本身不会被copy的,仅仅是它的内容。
- 如果
<src>
是一个本地的 tar 压缩包,使用的是支持的压缩格式(本身,gzip,bzip2 或者 xz),它被解压之后是一个目录。远程URL的资源是不能解压的,无论是一个目录被copy还是被解压的时候,tar -x
有相同的动作,结果是:
1.在目的地无论存在什么
- 源的树形结构中内容解决冲突的时候都是优先选择后者,逐个文件的解决。
注意:无论一个文件是否被确定为可识别的压缩格式都会是基于文件内容完成,而不是文件名。例如,如果一个空文件以
.tar.gz
结尾,将不会被识别为一个压缩文件,不会生成任何的解压错误信息,而是文件只会被简单地copy到目的地。
如果
<src>
是一个任意其它类型的文件,它和元数据一起都会被copy。如果<dest>
以斜杠/
结尾,它将被认为是一个目录,<src>
的内容将会被写到<dest>/base(<src>)
。如果指定了多个
<src>
资源,不管使用的是目录还是通配符,<dest>
必须是一个目录,即它必须以斜杠/
结尾。如果
<dest>
不以斜杠结尾,它将被认为是一个普通的文件,<src>
的内容将会被写到<dest>
。如果
<dest>
不存在,在这个路径所有不存在的目录都会被创建。
01.9 COPY
COPY指令有两种形式:
COPY <src>... <dest>
-
COPY ["<src>",... "<dest>"]
(路径中包含空格的必须使用这种形式)
COPY
指令从<src>
copy新的文件和目录,然后将它们添加到容器的文件系统中,路径为<dest>
。
可以指定多个<src>
资源,但是它们必须是编译上下文的相对资源目录。
每一个<src>
都可以包含通配符,匹配方式使用的是Go语言的文件路径的匹配规则 filepath.Match,例如:
COPY hom* /mydir/ # adds all files starting with "hom"
COPY hom?.txt /mydir/ # ? is replaced with any single character, e.g., "home.txt"
<dest>
是一个绝对路径,或者是WORKDIR
的相对路径,source中指定的文件会被copy到目的的容器中。
COPY test relativeDir/ # adds "test" to `WORKDIR`/relativeDir/
COPY test /absoluteDir/ # adds "test" to /absoluteDir/
当添加的文件或者目录的路径中包含特殊字符(例如[
和]
)时,你需要根据Go语言的规则对它们进行转义,防止它们被当作是通配符,例如,copy文件名为arr[0].txt
的文件。使用下面的方式:
COPY arr[[]0].txt /mydir/ # copy a file named "arr[0].txt" to /mydir/
所有新的文件和目录在创建的时候都必须有一个0的UID和GID。
注意:如果你通过标准输入传递一个
Dockerfile
文件(docker build - < somefile
),这时是没有编译上下文的,因此COPY
指令是不能使用的。
任意的COPY
都可以接受一个--from=<name|index>
标识,它用于设置资源位置为前面编译的阶段(FROM .. AS <name>
创建的),而不使用用户发送的编译上下文。这个标识也接受前面所有编译阶段(以FROM
指令开始)分配的数字索引,如果找不到指定名称的编译阶段,则尝试使用同名的镜像。
COPY
指令遵循下面的规则:
-
<src>
的路径必须在编译的上下文中,不能使用COPY ../something /something
,因为docker build
的第一步是发送上下文目录(包括子目录)到docker的后台。 - 如果
<src>
是一个目录,目录下的所有内容都会被copy,包括文件系统的元数据。
注意:文件目录本身不会被copy的,仅仅是它的内容。
如果
<src>
是一个任意其它类型的文件,它和元数据一起都会被copy。如果<dest>
以斜杠/
结尾,它将被认为是一个目录,<src>
的内容将会被写到<dest>/base(<src>)
。如果指定了多个
<src>
资源,不管使用的是目录还是通配符,<dest>
必须是一个目录,即它必须以斜杠/
结尾。如果
<dest>
不以斜杠结尾,它将被认为是一个普通的文件,<src>
的内容将会被写到<dest>
。如果
<dest>
不存在,在这个路径所有不存在的目录都会被创建。
00.00 暂停一下
docker的文档写的太详细了,全写在一篇文章中太长了,忍痛分割一下吧!余下的部分指令参见《Dockerfile 参考手册(三):指令介绍》