此文根据 Dockerfile Reference 翻译而来,其中包含少许自己的理解,由于能力有限,难免有错漏,如有发现,请不吝指正,谢谢!
Docker可以从Dockerfile
中读取指令并自动构建镜像。Dockerfile
是一个文本文档,其中包含用户可以在命令行上调用的进行镜像装配的所有命令。使用docker build
,用户可以创建一个连续执行多个命令行指令的自动构建。
01 用法
docker build
命令从 Dockerfile
和一个 构建上下文(build context)
中构建docker镜像。构建上下文(build context)
是位于指定位置(PATH
或 URL
) 的文件集合。PATH
是本地文件系统上的一个目录,URL
是一个Git仓库的位置。
在 构建上下文
是递归处理的,所以,一个 PATH
包含所有子目录,而 URL
包含存储库及其子模块。
下面的例子展示了使用当前目录作为构建上下文进行镜像构建:
$ docker build .
Sending build context to Docker daumon 6.51 MB
构建由Docker守护进程执行,而不是命令行。构建过程所做的第一件事是将整个构建上下文(递归地)发送给守护进程。在大多数情况下,最好使用一个空目录作为构建上下文,并将Dockerfile保存在该目录中。并且只添加Dockerfile中的指令所需的文件。
警告:不要使用root目录
/
作为构建上下文,因为这样会导致硬盘上的所有内容都会被上传到Docker守护进程。
要使用构建上下文中的文件,必须在 Dockerfile
中的指令中指定文件的路径。要提高构建的性能,可以通过向构建上下文目录中添加.dockerignore文件来排除文件和目录。
一般来说,Dockerfile
的名称就是 Dockerfile
,且位于构建上下文的根目录中,不过,也可以在 docker build
中使用 -f
标志指向位于文件系统上任意位置的 Dockerfile
。
$ docker build -f /path/to/a/Dockerfile .
可以通过 docker build
的 -t
标志指定被成功创建的镜像要保存的仓库名称和标签:
$ docker build -t /shykes/myapp .
要在镜像构建成功之后将其标记到多个存储库,可以在 docker build
命令后添加多个 -t
标志。
$ docker build -t shykes/myapp:1.0.2 -t shykes/myapp:latest .
在Docker守护进程执行 Dockerfile
中的指令之前,它会执行 Dockerfile
的初步验证,如果语法不正确,则返回一个错误:
$ docker build -t test/myapp .
Sending build context to Docker daemon 2.048 kB
Error response from daemon: Unknown instruction: RUNCMD
Docker守护进程将按照 Dockerfile
中的指令的顺序,从上到下逐个执行,因此,应该根据需要合理安排 Dockerfile
中指令的顺序。每条指令都会创建一个新的镜像层(如果需要的话)并提交。Docker大体上按照如下流程执行 Dockerfile
中的指令:
docker commit
的操作,提交一个新的镜像层。Dockerfile
中的下一条指令,直到所有指令都执行完毕。从上面也可以看出,如果你的 Dockerfile
由于某些原因(如某条指令失败了)没有正常结束,那么你将得到了一个可以使用的镜像。这对调试非常有帮助:可以基于该镜像运行一个具备交互功能的容器,使用最后创建的镜像对失败的指令进行调试。
只要可能,Docker将重用中间镜像(缓存),显著加快Docker构建过程。这由控制台输出中的Using cache消息进行指示。
$ docker build -t svendowideit/ambassador .
Sending build context to Docker daemon 15.36 kB
Step 1/4 : FROM alpine:3.2
---> 31f630c65071
Step 2/4 : MAINTAINER [email protected]
---> Using cache
---> 2a1c91448f5f
Step 3/4 : RUN apk update && apk add socat && rm -r /var/cache/
---> Using cache
---> 21ed6e7fbb73
Step 4/4 : CMD env | grep _TCP= | (sed 's/.*_PORT_\([0-9]*\)_TCP=tcp:\/\/\(.*\):\(.*\)/socat -t 100000000 TCP4-LISTEN:\1,fork,reuseaddr TCP4:\2:\3 \&/' && echo wait) | sh
---> Using cache
---> 7ea8aef582cc
Successfully built 7ea8aef582cc
构建缓存仅用于具有本地父链的映像。这意味着这些镜像是由以前的构建创建的,或者整个镜像链是用 docker load
加载的。如果希望使用特定映像的构建缓存,可以使用——cache-from选项指定。使用 cache-from
指定的映像不需要有父链,并且可以从其他注册中心提取。
构建完成后,就可以考虑将存储库推送到其注册中心了。
02 格式
如下,即为 Dockerfile
的格式:
# Comment
INSTRUCTION arguments
Dockerfile
中的指令是不区分大小写的。然而,习惯上要将它大写,以便更容易地与参数区分开来。
Docker按照Dockerfile
中指令的顺序从上到下逐一执行。一般来说,Dockerfile
必须以指定用于构建过程的基础镜像的 FROM
指令开始,而 ARG
指令是唯一可以在 FROM
指令之前使用的指令,FROM
指令之前可以有一条或多条 ARG
指令,用于声明 Dockerfile
中的 FROM
指令中可以使用的参数。
Docker将以 #
开始的行作为注释,除非该行是有效的解析器指令。一行中任何其他地方的 #
标记都将被视为参数。
因此,下面的注释即为一条合法的注释:
# Comment
RUN echo 'we are running some # of cool things'
注意:注释中不支持行延续字符
03 解析器指令
解析器指令是可选的,并且会影响 Dockerfile
中后续行处理的方式。解析器指令不会向构建中添加镜像层,也不会显示为构建步骤。解析器指令是以 # directive=value
形式的特殊注释类型编写的。一条指令只能使用一次。
一旦注释、空行或构建器指令被处理,Docker就不再寻找解析器指令。相反,它会将所有解析器指令格式的内容均视为注
释,并且不尝试验证它是否可能是解析器指令。因此,所有解析器指令必须位于Dockerfile的最顶端。
解析器指令不区分大小写,但习惯上将其写为小写,并且一般会在解析器指令后添加一个空白行。在解析器指令中不支持行延续字符。
由于这些规则,下面的例子都是无效的:
由于行延续而无效:
# direc \
tive=value
因为出现了两次而无效:
# directive=value1
# directive=value2
FROM ImageName
因为出现在构建指令之后而无效:
FROM ImageName
# directive=value
因为出现在注释之后被当作注释而无效:
# About my dockerfile
# directive=value
FROM ImageName
未知的解析器指令因为未被识别而被当作注释。另外,已知的解析器指令因为出现在被当作注释的未知解析器指令之后也被当成了注释:
# unknowndirective=value
# knowndirective=value
解析器指令中允许使用非断行空格。因此,以下几行都被同等对待:
#directive=value
# directive =value
# directive= value
# directive = value
# dIrEcTiVe=value
Dockerfile
中支持如下的解析器指令:
04 转义指令
# escape=\ (backslash)
或者
# escape=` (backtick)
escape
指令用于转义Dockerfile中的字符。默认转义字符是 \
。
转义字符既用于转义一行中的字符,也用于转义换行符。这使得一条 Dockerfile
指令可以跨越多行。注意,无论解析器指令 escape
是否包含在 Dockerfile
中,转义都不会在 RUN
指令中执行,除非要转义的字符出现在行末尾。
在Windows环境下,将转义字符设置为反引号而不是 \
将非常有用,这样,\
就可以保持其作为目录路径分隔符的原有的含义,与 Windows PowerShell
中保持一致。
考虑下面的这个例子,它在Windows上可能会非常诡异地执行失败。第二行末尾的第二个 \
将被视为用来转义换行符的转义字符,而不是第一个 \
的转义目标。类似地,假设第三行末尾的 \
本来是要作为一条指令进行处理的,但是它却将被视为行延续字符。这个 Dockerfile
的结果是,第二行和第三行被认为是一条指令:
FROM microsoft/nanoserver
COPY testfile.txt c:\\
RUN dir c:\
那么,结果将是:
PS C:\John> docker build -t cmd .
Sending build context to Docker daemon 3.072 kB
Step 1/2 : FROM microsoft/nanoserver
---> 22738ff49c6d
Step 2/2 : COPY testfile.txt c:\RUN dir c:
GetFileAttributesEx c:RUN: The system cannot find the file specified.
PS C:\John>
解决上述问题的一种方式是使用 /
作为 COPY
指令和 DIR
指令的目标。然而,即便这样,语法也是比较令人困惑的,因为它看起来不像是Windows上常规的路径的写法,更糟糕的情况是,这样写很容易出错,因为并非Windows上的所有命令都支持 /
作为路径分隔符。
但是,如果使用 escape
解析器指令,下面的 Dockerfile
中的指令将按照预期成功执行,因为其中使用了更符合Windows平台下路径的书写方式:
# escape=`
FROM microsoft/nanoserver
COPY testfile.txt c:\
RUN dir c:\
执行结果如下:
PS C:\John> docker build -t succeeds --no-cache=true .
Sending build context to Docker daemon 3.072 kB
Step 1/3 : FROM microsoft/nanoserver
---> 22738ff49c6d
Step 2/3 : COPY testfile.txt c:\
---> 96655de338de
Removing intermediate container 4db9acbb1682
Step 3/3 : RUN dir c:\
---> Running in a2c157f842f5
Volume in drive C has no label.
Volume Serial Number is 7E6D-E0F7
Directory of c:\
10/05/2016 05:04 PM 1,894 License.txt
10/05/2016 02:22 PM <DIR> Program Files
10/05/2016 02:14 PM <DIR> Program Files (x86)
10/28/2016 11:18 AM 62 testfile.txt
10/28/2016 11:20 AM <DIR> Users
10/28/2016 11:20 AM <DIR> Windows
2 File(s) 1,956 bytes
4 Dir(s) 21,259,096,064 bytes free
---> 01c7f3bef04f
Removing intermediate container a2c157f842f5
Successfully built 01c7f3bef04f
PS C:\John>
05 解析环境变量
环境变量 (使用 ENV
指令声明) 可以在某些指令中用作可以被Dockerfile解析的变量。使用转义字符还可以将环境变量进行转义,使其表达其字面含义。
要在Dockerfile
中获取某个变量的值,可以使用 $variable_name
或 ${variable_name}
,它们的作用是相同的。大括号语法通常用于处理没有空格的变量名,如 ${foo}_bar
。
大括号语法也支持如下的标准的 bash
修饰符:
${variable:-word}
如果已设置 variable
环境变量,则取其值作为该表达式的结果,否则取 word
作为结果。${variable:+word}
如果已设置 variable
环境变量,则word
为该表达式的结果,否则该表达式的结果为空字符串。在上面的例子中,word
可以是任何字符串,或是其他环境变量。
可以在变量前加上转义字符 \
对其进行转义:例如 \$foo
或 \${foo}
,将被转义为表示 $foo
和 ${foo}
的字面量。
例如(经过解析的表达式显示在 #
之后)
FROM busybox
ENV foo /bar
WORKDIR ${foo} # WORKDIR /bar
ADD . $foo # ADD . /bar
COPY \$foo /quux # COPY $foo /quux
Dockerfile
中的如下指令支持环境变量:
此外还有:
注意:在1.4 版本之前,
ONBUILD
是不支持环境变量的,即使与上面列出的指令结合使用时。
环境变量替换将在整个指令中对每个变量使用相同的值,也就是说,只有当整个指令执行完毕,指令中被设置的环境变量才会生效,而不会在执行过程中,就使得对某个已经存在的环境变量的重新赋值生效。这句话可能不太好理解,我们用下面的例子来说明:
ENV abc=hello
ENV abc=bye def=$abc
ENV ghi=$abc
最终,def
的值将被设置为 hello
, 而不是 bye
,而 ghi
的值将被设置为 bye
,为什么呢?因为在第一个 ENV
指令中,环境变量 abc
的值被设置为字符串 hello
;在第二个 ENV
指令中,先将已经存在的环境变量 abc
的值重新设置为字符串 bye
,此时,整个 ENV
指令还未执行结束,环境变量 abc
的值其实还是字符串 hello
,然后再将环境变量 abc
的值赋给环境变量 def
,至此,第二个 ENV
指令执行完毕,环境变量 def
的值就是字符串 hello
了,而环境变量 abc
的值也变为字符串 bye
;而第三个 ENV
指令将环境变量 abc
的值赋给环境变量 ghi
,所以,环境变量 ghi
的赋值即为字符串 bye
。
06 .dockerignore文件
在 docker CLI
将构建上下文发送到Docker的守护进程之前,会检查构建上下文的根路径下是否存在名为 .dockerignore
的文件,如果这个文件存在,docker CLI
在上传将构建上下文时会把能被 .dockerignore
中的匹配模式匹配到的文件和目录排除在外。这有助于避免将一些大的或者敏感的文件和目录发送到Docker守护进程,或者使用 ADD
或者 COPY
指令添加那些文件和目录到镜像中。
docker CLI
将 .dockerignore
文件解释为一个以换行符分割的模式列表。在进行模式匹配时,构建上下文的根目录被当作工作目录和根目录。例如,模式 /foo/bar
和 foo/bar
的作用是相同的,都是排除 PATH
的子目录或通过 URL
指定的git存储库中名为 foo
的目录下面一个叫 bar
的文件或者目录。除此之外,不排除任何其它的文件和目录。
.dockerignore
文件中以 #
开头行被认为是注释,docker CLI
会将其忽略。
下面是一个 .dockerignore
文件的例子:
# comment
*/temp*
*/*/temp*
temp?
该文件将导致以下构建行为:
匹配模式 | 行为 |
---|---|
# comment |
忽略 |
*/temp* |
排除根目录下的子目录中以 temp 开头的文件或目录。例如,文本文件 /somedir/temporary.txt 会被排除,目录 /somedir/temp 也会被排除。 |
*/*/temp* |
排除根目录下深度为2的子目录中任何以 temp 开头的文件或目录。例如,文本文件 /first_dir/second_dir/temporary.txt 会被排除,目录 /first_dir/second_dir/.temp 也会被排除。 |
temp? |
排除根目录下名字以 temp 开头,且后接1个字符的文件和目录,例如 /tempa.txt 和 /tempb 均会被排除。 |
.dockerignore
文件中的模式匹配使用的是 Go
语言中的 filepath.Match
规则。在预处理步骤中,会使用 Go
语言中的 filepath.Clean
删除前导或后置的空格、 .
和 ..
。预处理步骤以后,空白行将被忽略。
除了 Go
语言中的 filepath.Match
规则,Docker还支持一个特殊的通配符 **
,这个通配符可以匹配任意数量的目录(包括 0个)。例如,**/*.go
将排除构建上下文根目录及其所有子目录下以 .go
结尾的文件及目录。
以感叹号 !
开始的行可用于排除的例外情况。下面是一个使用这种机制的例子:
*.md
!README.md
结果将会是除过 README.md
文件的所有其他 markdown
文件都会被从构建上下文中排除。
例外模式 !
的位置会影响 .dockerignore
中的模式匹配的行为:.dockerignore
中匹配特定文件的最后一行(非注释行)将决定其匹配到的文件是被包含还是排除。考虑下面的这个例子:
*.md
!README*.md
README-secret.md
结果将会是,所有名称不以 README
开头的 markdown
文件和 README-secret.md
这个文件都会被从构建上下文中排除。
再来看看下面这个例子:
*.md
README-secret.md
!README*.md
结果将会是,所有名称以 README
开头的markdown
文件都会被包含近构建上下文中。上例中,中间一行 README-secret.md
是不起作用的,因为 !README*.md
这个匹配模式匹配到的结果中包含 README-secret.md
。
甚至可以使用 .dockerignore
文件来排除 Dockerfile
和 .dockerignore
文件。但是这些文件仍然会被发送到Docker守护进程,因为Docker守护进程需要这些文件来正常工作。但是 ADD
和 COPY
指令不会将它们复制到镜像中去。
最后,您可能希望直接指定在构建上下文中包含哪些文件,而不是排除哪些文件。要实现这一点,请指定 *
作为第一个匹配模式,然后是一个或多个 !
例外模式。
注意:由于历史原因,
.
这种匹配模式将会被忽略。