Dockerfile 参考手册(一)
本文于351天之前发表,文中内容可能已经过时。
最近学习docker过程中,发现Dockerfile是一个非常重要的文档,本文系统学习一下。
文档是基于Docker v17.09 版本。
翻译作品,原文请见官网英文文档。
00 前言
Docker可以读取Dockerfile
中的指令自动构建镜像,Dockerfile
是一个文本文件,它包含很多命令,用户可以在命令行上调用这些命令组装镜像。用户可以使用docker build
来自动构建镜像,它可以连续执行若干命令行指令。
本文将介绍在Dockerfile
中你可以使用命令,你读完这篇文章之后,Dockerfile
Best Practices 是另一篇很好的指导。
01 用法
docker build
命令根据Dockerfile
和上下文来构建镜像,构建过程的上下文是通过PATH
或者URL
指定的一系列文件。PATH
是一个本地文件系统的目录,URL
是一个Git仓库的位置。
上下文是一个递归的处理过程。因此,PATH可以包含任何的子目录,
URL`包括仓库和它的子模块。下面是一个构建镜像的命令的示例,使用当前目录作为上下文:
$ docker build .
Sending build context to Docker daemon 6.51 MB
...
Build是通过Docker daemon(docker 守护进程),而不是 CLI(命令行界面)执行的。Build过程要做的第一件事是发送整个上下文(递归)到Docker的守护进程。最佳实践是,开始创建一个空的文件夹作为上下文,然后将你的Dockerfile文件放在那个文件夹下,仅添加一些你在编译Dockerfile过程中需要的文件。
注意:千万不要使用根路径
/
作为PATH
,这将导致Build会发送你的硬盘上的所有内容到Docker的守护进程。
在Build的上下文中为了使用Dockerfile中指定的一个文件,这个文件是某个指令(例如COPY
指令)用到的。为了提高Build的性能,通过添加.dockerignore
文件,可以排除上下文目录中的某些文件和目录,关于如何创建.dockerignore
文件更多信息见本文的下面章节。
一般认为,Dockerfile
文件都应该位于上下文的根目录下,你可以在docker build
后使用-f
标识来指定你的文件系统中任意位置的Dockerfile文件。
$ docker build -f /path/to/a/Dockerfile .
你还可以指定用来存储成功编译的镜像文件的仓库和标签:
$ docker build -t shykes/myapp .
Build的时候也可以为镜像添加多个仓库标签,在你执行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
中的指令的,如果需要的话,会提交每个指令的结果到新的镜像中,最后输出新镜像的的ID。Docker的守护进程也会自动清除你发送的上下文。
注意,每一条指令都是独立执行的,因此在创建一个镜像的时候,RUN cd /tmp
这条指令不会对下一条指令有任何影响。
无论任何可能的时候,Docker都将会重用中间状态(缓存)的镜像,这样能够明显地加速docker build
的过程,这是通过控制台输出的信息Using cache
来标识的。(更多信息参见,在Dockerfile
的最佳实践指导中的Build cache section):
$ 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 SvenDowideit@home.org.au
---> 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
仅在编译那些有具有本地主链的镜像时使用缓存,意思是这些镜像的创建依赖前面的Build,或者整个镜像链都已经通过docker load
加载进来了。如果你希望对一个指定镜像使用build cache,你可以使用--cache-from
来指定,通过--cache-from
指定的镜像不需要有一个主链,也可能是从其他的中心拉取的。
当你编译完成的时候,你该学习 Pushing a repository to its registry。
02 格式
下面是Dockerfile
文件的格式:
# Comment
INSTRUCTION arguments
指令对字母大小写是不敏感的,但是,习惯上将它们大写,以便容易和参数区分开。
Docker是按照顺序来执行Dockerfile
中的指令的。一个Dockerfile
文件必须以FROM
指令开始,FROM
指令指定了你正在编译镜像的基础镜像。在Dockerfile
文件中,FROM
指令的前面仅可以是一个或者多个ARG
指令,这些声明的参数被用于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
下面的转义指令是支持的:escape
04 转义符指令
# escape=\ (backslash)
或者
# escape=` (backtick)
escape
指令是用来设置Dockerfile中转义字符的字符,如果不指定的话,默认的转义字符是\
。
转义字符不仅用在一行中的转义字符上,也用在开启一个新行。Dockerfile
中指令允许是多行的。注意,无论在Dockerfile
中是否包含escape
转义指令,在RUN
命令中是不会执行转义的,除非是在一行的末尾。
在Windows环境下,设置转义字符为 `
,是非常有用的,由于\
是目录路径的分隔符,`
和windows下的转义字符是一致的。
考虑下面的一个例子,在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
。${variable_name}
语法也支持一些标准的bash
修饰符,例如下面:
${variable:-word}
意思是,如果variable
被设置了,结果将是那个值,如果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
指令中都是支持的:
ADD
COPY
ENV
EXPOSE
FROM
LABEL
STOPSIGNAL
USER
VOLUME
WORKDIR
此外还有:
ONBUILD
(当与上面任何一个指令结合时)
注意:在1.4版本之前,
ONBUILD
是不支持环境变量的,即使与上面列出的指令结合时。
在整个指令中环境变量的替换值都是用同一个值,换句话说,就是下面的例子:
ENV abc=hello
ENV abc=bye def=$abc
ENV ghi=$abc
结果是,def
的值是hello
,而不是bye
,ghi
的值是bye
,因为它不是设置abc
为bye
的指令的一部分。
06 .dockerignore文件
在docker命令行界面中发送上下文到docker的守护进程之前,它会检查上下文目录根路径下名为.dockerignore
的文件,如果这个文件存在,命令行界面会修改上下文,排除那些被.dockerignore
中的模式匹配到的文件和目录。这有助于避免一些不必要的(大的或者敏感的文件和目录)发送到守护进程,还能避免一些潜在的使用ADD
或者 COPY
添加文件和目录到镜像中。
命令行解释.dockerignore
文件为一个换行符分割的模式列表,类似于Unix shell的glob文件。由于这个匹配的目的,上下文的根被认为是工作目录和根目录。例如,模式 /foo/bar
和foo/bar
都是在排除目录foo
下面一个叫bar
的文件或者目录,目录foo
是PATH
的子目录或者URL
指定的git仓库下的子目录。不排除任何其它的。
如果在.dockerignore
文件中有一行以#
开头,那么这一行被认为是注释,命令行解释之前为忽略它。
下面是一个.dockerignore
文件的例子:
# comment
*/temp*
*/*/temp*
temp?
这个文件将引发下面的构建行为:
规则 | 行为 |
---|---|
# comment |
忽略。 |
*/temp* |
排除根目录下的子目录中任何以temp 开头的文件和目录,例如,/somedir/temporary.txt 这个文本文件会被排除,/somedir/temp 这个目录也会被排除。 |
*/*/temp* |
排除来自子目录的任何以temp 开头的文件和目录,这个子目录是根目录下两层,例如,/somedir/subdir/temporary.txt 被排除的。 |
temp? |
排除那些根目录下名字以temp 开始拓展一个字符的文件和目录,例如,/tempa 和 /tempb 是被排除的。 |
完成这个匹配使用的是Go语言的文件路径匹配规则,在预处理步骤中会去除掉开头和结尾的空格,并清除.
和..
元素,在这个过程中使用的是Go语言的文件路径清理方法,预处理过程中会忽略掉空白行。
在Go语言的文件路径匹配规则之外,Docker还支持一个特殊的通配符**
,用于匹配任意数量的目录(包括零),例如,**/*.go
将排除所有以.go
结尾的文件,它会在编译上下文的根目录的所有目录中找。
以感叹号!
开始的行被用于标出排除中的异常文件,下面的这个.dockerignore
文件的例子就使用了这种机制:
*.md
!README.md
在上下文中除了README.md
之外,所有markdown文件都会被排除。
异常规则!
的位置影响行为:.dockerignore
文件的最后一行匹配一个特定文件,它是包含还是排除呢?看下面的例子:
*.md
!README*.md
README-secret.md
`
除了README 文件之外,没有任何markdown文件被包含进上下文,并没有README-secret.md
。
现在看这个例子:
*.md
README-secret.md
!README*.md
所有的README文件都会被包含进去,中间一行是没有任何影响的,因为!README*.md
能够匹配 README-secret.md
,并且在后面。
你甚至可以用.dockerignore
来排除Dockerfile
文件和.dockerignore
,这些文件仍然是会被送到守护进程的,因为需要它们做这些工作,但是ADD
和COPY
指令是不会copy它们到镜像中去的。
最后,你可能想要指定文件包含进上下文,而不是排除它们,为了实现这个目的,可以使用*
作为第一个模式,下面使用一个或者多个!
异常模式。
注意:由于历史原因,模式
.
是被忽略的。到此为止介绍Dockerfile文件中工作原理和一些语法,以及相关的一些东西,其中03和04节不太常用,翻译不是太好,请高手指正。Dockerfile中常用的指令下一篇文章再介绍。