学习Docker的时候遇到了不少问题,也有同事询问的时候不能回答上来,所以系统的记录一些Docker原理方面的学习过程。先从dockerfile入手,本篇主要是官网文档的翻译。
Docker的内核基础
提到Docker,基本都知道其本质是宿主机上面的一个进程,通过namespace实现了资源隔离。通过cgroups实现了资源限制,通过写时复制(copy-on-write)实现了高效的文件操作。从Linux内核3.8版本开始,提供了namespace功能,主要分为以下六项隔离:
- UTS:主机名与域名
- IPC:信号量、消息队列和共享内存
- PID:进程编号
- Network:网络设备、网络栈、端口等
- Mount:挂载点(文件系统)
- User:用户和用户组
Docker自底向上的结构
构建一个Docker应用可以分为以下三层:
- Stack
- Service
- Container
使用Dockerfile定义一个容器
Dockerfile定义了你的容器内的环境发生了什么。在这个环境里,资源的获取比如网络接口或者磁盘驱动都是虚拟化的,并且与你系统的其他部分是隔离开的,所以你必须将端口映射到外面,而且你必须制定那些文件需要“复制”到这个环境以内。
Docker可以通过Dockerfile的命令构建一个镜像,使用Docker build命令可以创建一个连续的命令行指令进行自动构建。
Dockerfile指南
用法
docker build
命令从Dockerfile和上下文中构建镜像。构建的上下文是在特定位置的文件的集合,比如PATH和URL,PATH是你本地文件系统的目录,URL是git仓库地址。
上下文是递归处理的,所以PATH包括了子目录,URL也包括了仓库和它的子模块。比如把整个当前路径都作为上下文:
1 | $ docker build . |
2 | Sending build context to Docker daemon 6.51 MB |
3 | ··· |
为了在构建上下文的时候使用一个文件,Dockerfile使用一个命令去指定某个文件,比如COPY命令。为了增加构建过程的性能,可以通过添加.dockerignore来排除某些文件。
通常情况下,Dockerfile就叫Dockerfile,你可以使用-f
命令指定使用某个Dockerfile,用法如下:
1 | docker build -f /path/to/a/Dockerfile . |
如果构建成功,你也可以指定一个仓库和标签来说明在哪里存储你的镜像:
1 | docker build -t shykes/myapp . |
为了在构建完成之后,给镜像标注多个仓库,可以添加多个-t
:
1 | docker build -t shykes/myapp:1.0.2 -t shykes/myapp:latest . |
在Docker守护进程执行Dockerfile中的命令之前,它会执行一个初步的验证,如果语法不正确会返回错误:
1 | $ docker build -t test/myapp . |
2 | Sending build context to Docker daemon 2.048 kB |
3 | Error response from daemon: Unknown instruction: RUNCMD |
Docker守护进程会依次逐条执行Dockerfile中的命令,在返回最终镜像的ID之前,如果需要,每一条命令的结果都会提交成为一个新的镜像。Docker守护进程会自动清理你发送的上下文。
需要注意,每一条命令都是独立运行的,而且会导致新的镜像被创建,所以RUN cd /tmp
不会在对下一条命令产生任何影响。
如果可能,Docker会重新利用中间镜像(缓存),从而让docker build过程显著加快。你可以从控制台输出中看到Using cache
的提示。(想要了解更多信息,参照Dockerfile最佳实践中的构建缓存部分)
1 | $ docker build -t svendowideit/ambassador . |
2 | Sending build context to Docker daemon 15.36 kB |
3 | Step 1/4 : FROM alpine:3.2 |
4 | ---> 31f630c65071 |
5 | Step 2/4 : MAINTAINER SvenDowideit@home.org.au |
6 | ---> Using cache |
7 | ---> 2a1c91448f5f |
8 | Step 3/4 : RUN apk update && apk add socat && rm -r /var/cache/ |
9 | ---> Using cache |
10 | ---> 21ed6e7fbb73 |
11 | 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 |
12 | ---> Using cache |
13 | ---> 7ea8aef582cc |
14 | Successfully built 7ea8aef582cc |
构建缓存只有在那些拥有本地父母链(local parent chain)的镜像中被使用。这意味着那些镜像是被前一阶段的构建产物所创造,或者被docker load
装载的整个镜像链条所创造。如果你想指定某一个镜像使用构建缓存,你可以用--cache-from
选项来指定。
完成你的构建过程之后,你就可以准备浏览将存储仓库推入注册
格式
Dockerfile的格式是这样的:
1 | # Comment |
2 | INSTRUCTION arguments |
指令是大小写不敏感的,但是惯例是把指令都写成大写,方便与参数区分开。
Docker会按照顺序执行Dockerfile中的指令,一个Dockerfile必须从FROM
命令开始,FROM
命令指定了一个你要构建的基础镜像。
Docker会把以#
为起始的一行视为注释,除非这一行是一个有效的解析指令(parse directives)。一个在其他位置出现的#
会被当做是参数的一部分,所以允许这样的语句:
1 | # Comment |
2 | RUN echo 'we are running some # of cool things' |
解析指令 Parse directives
解析指令是可选的,而且会影响Dockerfile中后续指令行的处理方式。解析指令不会增加新的构建层数,也不会被认为是一个构建步骤。它的写法是类似于特殊的注释:# directive=value
一个解析指令只被使用一次。
每当一个注释、空行或者构建指令被处理之后,Docker不会再去寻找解析指令。取而代之的是它会把解析指令格式的命令行视为一个注释,而且不会尝试去验证这是否是一个解析指令。因此,所有的解析指令都应该在Dockerfile的最上方。
解析指令大小写不敏感,按照惯例我们写成小写格式,而且后面添加一个空行。解析指令不支持行延长符号,所以如下是无效的:
1 | # direc \ |
2 | tive=value |
重复出现两次也是无效的:
1 | # directive=value1 |
2 | # directive=value2 |
3 | |
4 | FROM ImageName |
出现在构建指令之后的解析指令会被当做普通注释:
1 | FROM ImageName |
2 | # directive=value |
出现在普通注释之后的解析指令也会被当做普通注释:
1 | # About my dockerfile |
2 | # directive=value |
3 | FROM ImageName |
非法的解析指令会被当做普通注释,此外,紧随其后的合法解析指令也会被当做注释,因为出现在一条注释之后。
1 | # unknowndirective=value |
2 | # knowndirective=value |
不换行空格允许出现在解析指令之中。因此,下列行被认为已知:
1 | #directive=value |
2 | # directive =value |
3 | # directive= value |
4 | # directive = value |
5 | # dIrEcTiVe=value |
支持以下解析指令:escape
escape
1 | # escape=\ (backslash) |
或
1 | # escape=` (backtick) |
escape
指令指定Dockerfile中的转义字符,如果没有指定,默认的转义字符是\
.
转义字符不仅作用在行中的转义字符,也作用在换行符。这允许一个Dockerfile指令跨越多行。需要注意,无论escape解释语句是否出现在Dockerfile中,转义不会在RUN
命令中生效,除非在行尾。
将转义字符设置为`在Windows中尤其有用,因为\是地址分隔符,`与Windows PowerShell相一致。
考虑到下面的例子会在Windows中以一个不明显的方式失败。在第二行结束位置的第二个\会被当做换行符,而不是第一个\转义的目标。同样,在第三行末尾位置的\,假设它实际上是作用为一个指令,他被当做一个行延长符。这个Dockerfile的结果是第二行和第三行被当做一个单独的指令。
1 | FROM microsoft/nanoserver |
2 | COPY testfile.txt c:\\ |
3 | RUN dir c:\ |
结果:
1 | PS C:\John> docker build -t cmd . |
2 | Sending build context to Docker daemon 3.072 kB |
3 | Step 1/2 : FROM microsoft/nanoserver |
4 | ---> 22738ff49c6d |
5 | Step 2/2 : COPY testfile.txt c:\RUN dir c: |
6 | GetFileAttributesEx c:RUN: The system cannot find the file specified. |
7 | PS C:\John> |
一个解决方案是使用/
作为COPY
指令和dir
指令的目标。然而最好的情况下,这个语法在Windows上也并不自然,令人困惑,在比较坏的情况下,在Windows上使用/
作为地址分隔符有可能出现错误。
通过添加escape解析指令,下面的Dockerfile成功的使用原生的系统地址分割语法如期执行:
1 | # escape=` |
2 | |
3 | FROM microsoft/nanoserver |
4 | COPY testfile.txt c:\ |
5 | RUN dir c:\ |
结果如下:
1 | PS C:\John> docker build -t succeeds --no-cache=true . |
2 | Sending build context to Docker daemon 3.072 kB |
3 | Step 1/3 : FROM microsoft/nanoserver |
4 | ---> 22738ff49c6d |
5 | Step 2/3 : COPY testfile.txt c:\ |
6 | ---> 96655de338de |
7 | Removing intermediate container 4db9acbb1682 |
8 | Step 3/3 : RUN dir c:\ |
9 | ---> Running in a2c157f842f5 |
10 | Volume in drive C has no label. |
11 | Volume Serial Number is 7E6D-E0F7 |
12 | |
13 | Directory of c:\ |
14 | |
15 | 10/05/2016 05:04 PM 1,894 License.txt |
16 | 10/05/2016 02:22 PM <DIR> Program Files |
17 | 10/05/2016 02:14 PM <DIR> Program Files (x86) |
18 | 10/28/2016 11:18 AM 62 testfile.txt |
19 | 10/28/2016 11:20 AM <DIR> Users |
20 | 10/28/2016 11:20 AM <DIR> Windows |
21 | 2 File(s) 1,956 bytes |
22 | 4 Dir(s) 21,259,096,064 bytes free |
23 | ---> 01c7f3bef04f |
24 | Removing intermediate container a2c157f842f5 |
25 | Successfully built 01c7f3bef04f |
26 | PS C:\John> |
环境替换
环境变量(通过ENV
命令声明的)也可以在用在某一指令中,像一个变量一样被Dockerfile解释。转义也被用作将类变量语法逐字逐句的包含到声明中。(Environment variables (declared with the ENV statement) can also be used in certain instructions as variables to be interpreted by the Dockerfile. Escapes are also handled for including variable-like syntax into a statement literally.)
环境变量在Dockerfile中会表示为$variable_name
或者${variable_name}
。这两种表述方式等价,其中大括号方式通常被用作表述没有空格的变量名字,像${foo}_bar
。
${variable_name}
这种语法同样支持几种标准的bash编辑方式如下:
${variable:-word}
表示如果变量是一个集合,那么结果就是集合的值,否则结果是word。${variable:+word}
表示如果变量是一个集合,那么结果是word,否则是空字符串。
在所有情况下,word
可以使任何字符串,包括额外的环境变量。
可以通过在变量前添加\
进行转义:\$foo
或者\${foo}
。比如下面的例子,将会对$foo
和${foo}
逐字严格各自转换。
1 | FROM busybox |
2 | ENV foo /bar |
3 | WORKDIR ${foo} # WORKDIR /bar |
4 | ADD . $foo # ADD . /bar |
5 | COPY \$foo /quux # COPY $foo /quux |
环境变量在所有下列指令中被支持:
ADD
COPY
ENV
EXPOSE
FROM
LABEL
STOPSIGNAL
USER
VOLUMN
WORKDIR
同样的:
ONBUILD
(与其他上述指令一同使用的时候)
在整个指令中,环境变量替代物会为每一个变量使用同一个值。如下所示:
1 | ENV abc=hello |
2 | ENV abc=bye def=$abc |
3 | ENV ghi=$abc |
结果是def的值为hello,而不是bye。然而,ghi的值是bye因为这不是将abc设置为bye的那条语句。
.dockerignore file
在docker命令行把上下文发送给docker守护进程之前,它会在上下文路径的根目录下搜索文件名为.dockerignore的文件。如果文件存在,命令行就会把符合的文件从上下文中排除出去。
.dockerignore文件的示例如下:
1 | # comment |
2 | */temp* |
3 | */*/temp* |
4 | temp? |
这个文件会产生如下构建行为:
命令|行为
—|—#comment
|忽略*/temp*
|排除根路径下所有直接子目录内文件名或者目录名以temp
为开头的文件或文件夹。比如:/somedir/temporary.txt
就被排除了,或者路径/somedir/temp
。*/*/temp*
|排除所有根路径下二级子目录中以temp
为起始文件名的文件或文件夹。比如:/somedir/subdir/temporary.txt
。temp?
|排除根目录下文件名或目录名是temp
后面加一个字符。比如:/tempa
和tempb
。
具体用法和.gitignore类似,不再赘述。
FROM
1 | FROM <image> [AS <name>] |
或者
1 | FROM <image>[:<tag>] [AS <name>] |
或者
1 | FROM <image>[@<digest>] [AS <name>] |
FROM
指令初始化了一个新的构建阶段,而且为后续的指令准备了基础镜像。所以,一个有效的Dockerfile应该以FROM
指令起始。镜像可以是任何有效的镜像,尤其简单的是可以从公共仓库下载镜像作为开始。
ARG
是唯一可以出现在FROM
之前的指令。- 在一个Dockerfile中,
FROM
可以出现多次,创建多个镜像,或者把一个构建阶段作为另一个的依赖。只需要注意,在一条新的FROM
指令提交之前把上一个的镜像ID记录下来。每一个FROM
指令会清除之前指令创造的所有状态。 - 可选的,使用
AS name
语句可以赋予新的构建阶段可以一个名称。这个名称还可以在随后的FROM
和COPY --from=<name|index>
中使用以指定某个镜像。 tag
或者digest
选项是可选的。如果你都省略了,系统会缺省设置latest
标签。如果找不到任何tag
的值,构建器会返回一个错误。
理解ARG和FROM是如何相互需作用的
FROM
指令支持那些由ARG
声明的,出现在自身之前的变量。
1 | ARG CODE_VERSION=latest |
2 | FROM base:${CODE_VERSION} |
3 | CMD /code/run-app |
4 | |
5 | FROM extras:${CODE_VERSION} |
6 | CMD /code/run-extras |
一个FROM
指令之前的ARG
声明是独立于构建阶段之外的,所以不能在FROM
之后的任何指令中使用。想要使用FROM
指令之前的ARG
指令的默认属性,你需要在构建阶段之内再使用一次不带赋值的ARG
指令:
1 | ARG VERSION=latest |
2 | FROM busybox:$VERSION |
3 | ARG VERSION |
4 | RUN echo $VERSION > image_version |
RUN
RUN命令有两种格式;
RUN <command>
(shell格式,命令在shell中执行,默认是Linux中的/bin/sh -c
后者Windows中的cmd /s /c
)RUN ["executable", "param1", "param2"]
(执行格式)
RUN
指令会在当前镜像之上的新分层中执行任何命令,然后提交结果。产生的被提交镜像会在Dockerfile中的后续步骤中使用。
分层的RUN
指令和不断产生的提交符合Docker的核心思想:提交应当是简易的,容器可以从镜像的历史中的任何一个时间点创建,这点很像代码控制。
执行格式避免了shell字符歧义,而且你可以不指定使用特定的shell可执行文件(bash or sh or ?)的情况下使用RUN
指令。
你可以在shell格式中使用SEHLL
命令指定使用特定的shell。
在shell格式中你可以使用\
(反斜杠)来在多行中延续一条RUN指令,比如:
1 | RUN /bin/bash -c 'source $HOME/.bashrc; \ |
2 | echo $HOME' |
等价于:
1 | RUN /bin/bash -c 'source $HOME/.bashrc; echo $HOME' |
注意: 要使用不同的shell,而不是’/bin/sh’,请使用在所需shell中传递的exec形式。例如:
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上特别相关,其中反斜杠是路径分隔符。因为不是有效的JSON,并且以意外的方式失败,以下行将被视为shell形式:
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最佳实践中看到更多的信息。
ADD
指令也会使RUN
指令的缓存失效,详情见下。
CMD
CMD
指令有三种格式:
- CMD [“executable”,”param1”,”param2”](执行格式,这也是首选的格式)
- CMD [“param1”,”param2”](作为ENTRYPOINT的默认参数)
- CMD command param1 param2(shell格式)
Dockerfile中只能有一条CMD命令,如果有多条,那么只有最后一条生效。
CMD指令的主要用意是提供一个执行容器的默认值。这些默认值可以包括一个可执行文件,或者他们可以省略可执行文件。在这种情况下你必须指定一个ENTRTPOINT指令。
注意: 如果使用CMD为ENTRYPOINT指令提供默认参数,CMD和ENTRYPOINT指令都应以JSON数组格式指定。
注意: exec形式作为JSON数组解析,这意味着您必须在单词之外使用双引号(”)而不是单引号(’)。
注意: 与shell表单不同,exec表单不调用命令shell。这意味着正常的shell处理不会发生。例如,
CMD ["echo","$HOME"]
不会在$HOME上进行可变替换。如果你想要shell处理,那么使用shell形式或直接执行一个shell,例如:CMD ["sh","-c","echo $HOME"]
。当使用exec形式并直接执行shell时,正如shell形式的情况,它是做环境变量扩展的shell,而不是docker。
当你使用shell或者exec格式的时候,CMD指令会在容器启动的时候执行你输入的命令。
如果你使用CMD的shell格式,那么命令会以/bin/sh -c
的形式执行:
1 | FROM ubuntu |
2 | CMD echo "This is a test." | wc - |
如果你想执行你的命令但是不指定某个shell,你必须用JSON形式表述你的命令,而且给出一个可执行文件的完整地址。这种数组形式是CMD的首选格式。任何附加的参数必须单独表述为数组中的字符串:
1 | FROM ubuntu |
2 | CMD ["/usr/bin/wc","--help"] |
如果你想要你的容器每次都运行相同的可执行文件,那你应该考虑将ENTRYPOINT指令和CMD一起使用。
如果用户指定了docker run
的参数,那么这些参数就会覆盖掉CMD中指定的命令。
注意: 不用疑惑RUN和CMD的区别。RUN其实是运行了一条指令然后提交结果;CMD不在构建阶段执行任何指令,但是为镜像准备好指令(意思是说在docker run的时候执行)。
LABEL
1 | LABEL <key>=<value> <key>=<value> <key>=<value> ... |
LABEL指令添加镜像的元数据。一个LABEL就是一组键值对。想要在LABEL的值中添加空格,要像命令行中的转义那样使用引号和反斜杠,例子如下:
1 | LABEL "com.example.vendor"="ACME Incorporated" |
2 | LABEL com.example.label-with-value="foo" |
3 | LABEL version="1.0" |
4 | LABEL description="This text illustrates \ |
5 | that label-values can span multiple lines." |
一个镜像可以有多个标签,你可以在一行内指定多个标签。在Docker 1.10之前,这么做会减小最终镜像的大小,但是现在不再如此。你仍然可以选择在一条指令内指定多个标签,如下两个例子所示:
1 | LABEL multi.label1="value1" multi.label2="value2" other="value3" |
1 | LABEL multi.label1="value1" \ |
2 | multi.label2="value2" \ |
3 | other="value3" |
基础镜像或者双亲镜像(FROM中指定的镜像)中包含的标签会被你的镜像继承。如果一个标签已经存在但是有不同的值,会以最近设置的值为准,之前的会被覆盖。想要查看一个镜像的所有标签,可以使用docker inspect
命令。
1 | "Labels": { |
2 | "com.example.vendor": "ACME Incorporated" |
3 | "com.example.label-with-value": "foo", |
4 | "version": "1.0", |
5 | "description": "This text illustrates that label-values can span multiple lines.", |
6 | "multi.label1": "value1", |
7 | "multi.label2": "value2", |
8 | "other": "value3" |
9 | }, |
MAINTAINER(弃用)
标注作者名,已经弃用,推荐使用LABEL代替。
EXPOSE
1 | EXPOSE <port> [<port>/<protocol>...] |
EXPOSE指令提示Docker,容器会在运行时监听特定的网络端口。你可以指定端口是监听TCP还是UDP,而且如果没有指定,那么默认为TCP。
EXPOSE指令实际上不发布端口。它的作用是作为一个镜像构建者和容器使用者之间的文档,告知哪一个端口应当被发布。实际上想要在运行容器的时候发布端口,应当在docker run
命令中使用-p
参数以发布或者映射一个或者多个端口,或者用-P
参数发布所有暴露的端口并且映射它们到高优先级(high-order)的端口上。
想要建立宿主机的端口重定向,你需要看文档中的using -P flag。docker netword
命令支持在免于暴露特定端口的情况下建立容器之间的网络通信,因为容器可以通过任意的端口连接到网络。
ENV
1 | ENV <key> <value> |
2 | ENV <key>=<value> ... |
ENV指令把环境变量的key设为value。这个值会在所有Dockerfile的后续命令中存在,而且支持上面提到的环境变量替换。
ENV指令有两种格式,第一种格式,ENV <key> <value>
,会把一个单一的键设为某一值,在空格后面出现的整个字符串会被认为是值,包括空格和引号等字符。
第二种格式,ENV <key>=<value> ...
,允许一次性设置多个值。需要注意第二种方式的语法中使用等号,而第一种没有使用。就像命令行转义,引号和反斜杠可以用来包含值中的空格。
如下:
1 | ENV myName="John Doe" myDog=Rex\ The\ Dog \ |
2 | myCat=fluffy |
或
1 | ENV myName John Doe |
2 | ENV myDog Rex The Dog |
3 | ENV myCat fluffy |
两者作用相同,但是推荐使用第一种,因为只会产生一层缓存层。
ENV命令设置的环境变量会一直存在,包括容器从最终镜像运行后。你可以使用docker inspect
查看它们的状态,也可以使用docker run --env <key>=<value>
改变它们的状态。
注意: 环境变量的持续存留有可能产生不可预期的副作用。比如设置
ENV DEBIAN_FRONTEND noninteractive
有可能让基于Debian镜像的apt-get用户产生困惑。想要为单条命令设置一个值,使用RUN <key>=<value> <command>
。
ADD
ADD有两种格式:
ADD [--chown=<user>:<group>] <src>... <dest>
ADD [--chown=<user>:<group>] ["<src>",... "<dest>"]
(包含空格的路径需要使用这种格式)
注意:
--chown
特性只支持构建Linux容器的Dockerfile,而且不会在Windows容器中生效。由于用户和用户组概念不能再Linux和Windows之间转化,所以使用/etc/passwd
和/etc/group
把用户和用户组转化为ID这种特性只在基于Linux的容器下可行。
ADD指令从<src>
复制新的文件、路径或者远程文件URL到镜像的文件系统中的<dest>
位置。
可以指定多个<src>
资源,但是如果他们是文件或者目录的话,他们的路径应该是相对于构建上下文的。
每一个<src>
可以包括通配符,适配规则将按照Go语言的filepath.Match规则。比如:
1 | ADD hom* /mydir/ # 添加所有文件名以 "hom" 开头的文件 |
2 | ADD hom?.txt /mydir/ # ? 可以替代一个单个字符,比如, "home.txt" |
<dest>
是一个绝对路径,或者相对于WORKDIR
的路径,在其中,资源将被复制到目标容器。
1 | ADD test relativeDir/ # adds "test" to `WORKDIR`/relativeDir/ |
2 | ADD test /absoluteDir/ # adds "test" to /absoluteDir/ |
当添加一个包含特殊符号(比如[
和]
)的文件或者目录,你需要对这些字符进行符合Golang规则的转义,防止它们被视为一个统配规则。比如,想要添加一个文件名为arr[0].txt
的文件,你需要:
1 | ADD arr[[]0].txt /mydir/ # 复制一个名为 "arr[0].txt" 的文件到 /mydir/ |
一个新的文件或者目录被创造的时候,它的UID和GID将为0,除非用选项--chown
指定了用户和用户组或者UID/GID组合请求指定的内容添加所有权。--chown
选线的格式允许用户名或者用户组名以字符串或者整形形式的UID或GID组合。提供一个不带用户组的用户名或者不带GID的UID的话,会使用同样的UID作为GID。如果提供了一个用户名或者用户组名,容器的根目录文件系统/etc/passwd
和/etc/group
文件会被使用,来执行从名称到UID或GID的转换。下列栗子展示了合法的--chown
选项的定义:
1 | ADD --chown=55:mygroup files* /somedir/ |
2 | ADD --chown=bin files* /somedir/ |
3 | ADD --chown=1 files* /somedir/ |
4 | ADD --chown=10:11 files* /somedir/ |
如果容器的根目录文件系统不包含/etc/passwd
或者/etc/group
文件并且用户名和用户组名都没有使用--chown
选项,那么进行ADD操作的时候构建将会失败。使用数字的IDs不需要查找,而且对根文件系统的内容没有要求。
在<src>
是远程文件URL的情况下,目标将具有600的权限。如果正在检索的远程文件具有HTTP Last-Modified标头,则来自该标头的时间戳将用于设置目的地上的mtime文件。然而,像在ADD期间处理的任何其它文件一样,在决定文件是否被更改或者缓存是否被更新的时候,mtime不会被考虑进去。
注意: 如果通过传递一个Dockerfile通过STDIN(
docker build - <somefile
)构建,没有构建上下文,所以Dockerfile只能包含一个基于URL的ADD指令。您还可以通过STDIN传递压缩归档文件:(docker build - <archive.tar.gz
),归档根目录下的Dockerfile和归档的其余部分将在构建的上下文中使用。
注意: 如果您的URL文件使用身份验证保护,则您需要使用
RUN wget
,RUN curl
或从容器内使用其他工具,因为ADD指令不支持身份验证。
注意: 如果
<src>
的内容已更改,第一个遇到的ADD指令将使来自Dockerfile的所有后续指令的高速缓存无效。这包括使用于RUN指令的高速缓存无效。有关详细信息,请参阅Dockerfile最佳实践指南。
ADD遵循以下规则:
<src>
路径必须在构建的上下文中;你不能ADD ../something /something
,因为docker构建的第一步是发送上下文目录(和子目录)到docker守护进程。如果<src>
是URL并且<dest>
不以尾部斜杠结尾,则从URL下载文件并将其复制到<dest>
。- 如果
<src>
是URL并且<dest>
以尾部斜杠结尾,则从URL中推断文件名,并将文件下载到<dest>/<filename>
。例如,ADD http://example.com/foobar /
会创建文件/foobar
。网址必须有一个非平凡的路径,以便在这种情况下可以发现一个适当的文件名(http://example.com
不会工作)。 - 如果
<src>
是目录,则复制目录的整个内容,包括文件系统元数据。
注意: 目录本身不被复制,只是其内容。
- 如果
<src>
是识别的压缩格式(identity,gzip,bzip2或xz)的本地tar存档,则将其解包为目录。来自远程URL的资源不会解压缩。当目录被复制或解压缩时,它具有与tar -x
相同的行为:结果是以下的联合:- 无论目的地路径上存在什么,而且
- 原目标树的内容,冲突以逐个文件为基础解析为“2.”。
注意: 文件是否被识别为识别的压缩格式,仅基于文件的内容,而不是文件的名称。例如,如果一个空文件以.tar.gz结尾,则不会被识别为压缩文件,并且不会生成任何解压缩错误消息,而是将该文件简单地复制到目的地。
- 如果
<src>
是任何其他类型的文件,它会与其元数据一起单独复制。在这种情况下,如果<dest>
以尾部斜杠/
结尾,它将被认为是一个目录,并且<src>
的内容将被写在<dest>/base(<src>)
。 - 如果直接或由于使用通配符指定了多个
<src>
资源,则<dest>
必须是目录,并且必须以斜杠/
结尾。 - 如果
<dest>
不以尾部斜杠结尾,它将被视为常规文件,<src>
的内容将写在<dest>
。 - 如果
<dest>
不存在,则会与其路径中的所有缺少的目录一起创建。
COPY
COPY有两种格式:
COPY [--chown=<user>:<group>] <src>... <dest>
COPY [--chown=<user>:<group>] ["<src>",... "<dest>"]
(带空格的路径需要使用这种格式)
注意:
--chown
特性只支持构建Linux容器的Dockerfile,而且不会在Windows容器中生效。由于用户和用户组概念不能再Linux和Windows之间转化,所以使用/etc/passwd
和/etc/group
把用户和用户组转化为ID这种特性只在基于Linux的容器下可行。
COPY指令从<src>
复制新的文件、路径或者远程文件URL到容器的文件系统中的<dest>
位置。
可以指定多个<src>
资源,但是如果他们是文件或者目录的话,他们的路径应该是相对于构建上下文的。
每一个<src>
可以包括通配符,适配规则将按照Go语言的filepath.Match规则。比如:
1 | COPY hom* /mydir/ # 添加所有文件名以 "hom" 开头的文件 |
2 | COPY hom?.txt /mydir/ # ? 可以替代一个单个字符,比如, "home.txt" |
<dest>
是一个绝对路径,或者相对于WORKDIR
的路径,在其中,资源将被复制到目标容器。
1 | COPY test relativeDir/ # adds "test" to `WORKDIR`/relativeDir/ |
2 | COPY test /absoluteDir/ # adds "test" to /absoluteDir/ |
当添加一个包含特殊符号(比如[
和]
)的文件或者目录,你需要对这些字符进行符合Golang规则的转义,防止它们被视为一个统配规则。比如,想要添加一个文件名为arr[0].txt
的文件,你需要:
1 | COPY arr[[]0].txt /mydir/ # 复制一个名为 "arr[0].txt" 的文件到 /mydir/ |
一个新的文件或者目录被创造的时候,它的UID和GID将为0,除非用选项--chown
指定了用户和用户组或者UID/GID组合请求指定的内容添加所有权。--chown
选线的格式允许用户名或者用户组名以字符串或者整形形式的UID或GID组合。提供一个不带用户组的用户名或者不带GID的UID的话,会使用同样的UID作为GID。如果提供了一个用户名或者用户组名,容器的根目录文件系统/etc/passwd
和/etc/group
文件会被使用,来执行从名称到UID或GID的转换。下列栗子展示了合法的--chown
选项的定义:
1 | COPY --chown=55:mygroup files* /somedir/ |
2 | COPY --chown=bin files* /somedir/ |
3 | COPY --chown=1 files* /somedir/ |
4 | COPY --chown=10:11 files* /somedir/ |
如果容器的根目录文件系统不包含/etc/passwd
或者/etc/group
文件并且用户名和用户组名都没有使用--chown
选项,那么进行COPY操作的时候构建将会失败。使用数字的IDs不需要查找,而且对根文件系统的内容没有要求。
注意: 如果你构建的时候使用STDIN(
docker build - < somefile
),那么就没有构建上下文,所以COPY不能使用。
COPY可以选择添加--from=<name|index>
选项,用来设置源目标位置为前一个构建阶段(用FROM .. AS <name>创建
)从而用来代替用户发送的构建上下文。这个选项会接受一个数字索引,这个索引是从FROM指令开始所有之前的构建阶段分配的。
COPY遵循以下规则:
<src>
路径必须在构建的上下文中;你不能COPY ../something /something
,因为docker构建的第一步是发送上下文目录(和子目录)到docker守护进程。- 如果
<src>
是目录,则复制目录的整个内容,包括文件系统元数据。
注意: 目录本身不被复制,只是其内容。
- 如果
<src>
是任何其他类型的文件,它会与其元数据一起单独复制。在这种情况下,如果<dest>
以尾部斜杠/
结尾,它将被认为是一个目录,并且<src>
的内容将被写在<dest>/base(<src>)
。 - 如果直接或由于使用通配符指定了多个
<src>
资源,则<dest>
必须是目录,并且必须以斜杠/
结尾。 - 如果
<dest>
不以尾部斜杠结尾,它将被视为常规文件,<src>
的内容将写在<dest>
。 - 如果
<dest>
不存在,则会与其路径中的所有缺少的目录一起创建。
ENTRYPOPTINT
ENTRYPOINT有两种形式:
ENTRYPOINT ["executable", "param1", "param2"]
(exec形式,推荐)ENTRYPOINT command param1 param2
(shell形式)
一个ENTRYPOINT允许你配置一个将作为可执行文件的容器。
比如,下例将会启动一个默认内容的nginx,监听80端口:
1 | docker run -i -t --rm -p 80:80 nginx |
docker run <image>
的命令行参数将会在添加在exec格式的ENTRYPOINT的所有参数后面,而且会覆盖所有CMD指令所指定的参数。这允许参数通过,进入到入口点。比如docker run <image> -d
会传递-d
参数到入口点。你可以通过使用docker run --entrypoint
重写ENTRYPOINT指令。
shell格式阻止使用任何CMD或者run命令行参数,但是有缺点就是你的ENTRYPOINT将会从作为/bin/bash -c
的子命令开始,这就不会传递信号了。这意味着可执行文件不会成为容器的PID 1
,而且不会接受Unix信号,所以你的可执行文件不会收到docker stop <container>
的SIGTERM
。
只有Dockerfile中最后的ENTRYPOINT指令会生效。
Exec格式的ENTRYPOINT示例
你可以使用exec形式的ENTRYPOINT用于设置相当稳定的默认命令行和参数,然后使用任意形式的CMD指令设置额外的、可能被修改的默认命令。
1 | FROM ubuntu |
2 | ENTRYPOINT ["top", "-b"] |
3 | CMD ["-c"] |
当你运行一个容器,你可以看到top
是唯一的进程:
1 | $ docker run -it --rm --name test top -H |
2 | top - 08:25:00 up 7:27, 0 users, load average: 0.00, 0.01, 0.05 |
3 | Threads: 1 total, 1 running, 0 sleeping, 0 stopped, 0 zombie |
4 | %Cpu(s): 0.1 us, 0.1 sy, 0.0 ni, 99.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st |
5 | KiB Mem: 2056668 total, 1616832 used, 439836 free, 99352 buffers |
6 | KiB Swap: 1441840 total, 0 used, 1441840 free. 1324440 cached Mem |
7 | |
8 | PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND |
9 | 1 root 20 0 19744 2336 2080 R 0.0 0.1 0:00.04 top |
想要检查更多的结果,可以使用docker exec
:
1 | $ docker exec -it test ps aux |
2 | USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND |
3 | root 1 2.6 0.1 19752 2352 ? Ss+ 08:24 0:00 top -b -H |
4 | root 7 0.0 0.1 15572 2164 ? R+ 08:25 0:00 ps aux |
而且你可以优雅的使用docker stop test
请求关闭top
。
下例Dockerfile展示了使用ENTRYPOINT在前台运行Apache(作为PID 1
):
1 | FROM debian:stable |
2 | RUN apt-get update && apt-get install -y --force-yes apache2 |
3 | EXPOSE 80 443 |
4 | VOLUME ["/var/www", "/var/log/apache2", "/etc/apache2"] |
5 | ENTRYPOINT ["/usr/sbin/apache2ctl", "-D", "FOREGROUND"] |
如果你需要写一个开始脚本作为单独可执行文件,你可以保证最终的可执行文件通过exec
和gosu
命令接受到Unix信号:
1 | #!/usr/bin/env bash |
2 | set -e |
3 | |
4 | if [ "$1" = 'postgres' ]; then |
5 | chown -R postgres "$PGDATA" |
6 | |
7 | if [ -z "$(ls -A "$PGDATA")" ]; then |
8 | gosu postgres initdb |
9 | fi |
10 | |
11 | exec gosu postgres "$@" |
12 | fi |
13 | |
14 | exec "$@" |
最终,如果你需要在关闭的时候做一些额外的清理工作(或是与其他容器进行通讯),或者联合多个可执行文件,你可能需要确认ENTRYPOINT脚本能够接受到Unix信号并且传递他们,然后做更多工作:
1 | #!/bin/sh |
2 | # Note: I've written this using sh so it works in the busybox container too |
3 | |
4 | # USE the trap if you need to also do manual cleanup after the service is stopped, |
5 | # or need to start multiple services in the one container |
6 | trap "echo TRAPed signal" HUP INT QUIT TERM |
7 | |
8 | # start service in background here |
9 | /usr/sbin/apachectl start |
10 | |
11 | echo "[hit enter key to exit] or run 'docker stop <container>'" |
12 | read |
13 | |
14 | # stop service and clean up here |
15 | echo "stopping apache" |
16 | /usr/sbin/apachectl stop |
17 | |
18 | echo "exited $0" |
如果你用docker run -it --rm -p 80:80 --name test apache
命令运行这个镜像,你可以通过docker exec
检查容器的进程,或者用docker top
,然后请求脚本来停止Apache:
1 | $ docker exec -it test ps aux |
2 | USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND |
3 | root 1 0.1 0.0 4448 692 ? Ss+ 00:42 0:00 /bin/sh /run.sh 123 cmd cmd2 |
4 | root 19 0.0 0.2 71304 4440 ? Ss 00:42 0:00 /usr/sbin/apache2 -k start |
5 | www-data 20 0.2 0.2 360468 6004 ? Sl 00:42 0:00 /usr/sbin/apache2 -k start |
6 | www-data 21 0.2 0.2 360468 6000 ? Sl 00:42 0:00 /usr/sbin/apache2 -k start |
7 | root 81 0.0 0.1 15572 2140 ? R+ 00:44 0:00 ps aux |
8 | $ docker top test |
9 | PID USER COMMAND |
10 | 10035 root {run.sh} /bin/sh /run.sh 123 cmd cmd2 |
11 | 10054 root /usr/sbin/apache2 -k start |
12 | 10055 33 /usr/sbin/apache2 -k start |
13 | 10056 33 /usr/sbin/apache2 -k start |
14 | $ /usr/bin/time docker stop test |
15 | test |
16 | real 0m 0.27s |
17 | user 0m 0.03s |
18 | sys 0m 0.03s |
注意: 你可以使用
--entrypoint
来重写ENTRYPOINT,但是这只会设置二进制到exec(不会使用sh -c
)。
注意: exec格式是解析成JSON数组的格式,这意味着你必须使用双引号(“)包围文字,而不是用单引号(‘)。
注意: 与shell格式不同,exec格式不会调用shell命令,这意味着shell处理不会进行。比如,
ENTRYPOINT ["echo","$HOME"]
不会对$HOME
进行变量替换。如果你想要使用shell处理,那么就用shell格式或者直接执行shell,比如:ENTRYPOINT [ "sh", "-c", "echo $HOME" ]
。当使用exec形式并直接执行shell时,正如shell形式的情况,它是做环境变量扩展的shell,而不是docker。
Shell形式ENTRYPOINT的示例
您可以为ENTRYPOINT指定一个纯字符串,它将在/bin/sh -c
中执行。这中形式将使用shell处理来替换shell环境变量,并且将忽略任何CMD或docker run
命令行参数。要确保docker stop
将正确地发出任何长时间运行的ENTRYPOINT可执行文件,您需要记住用exec启动它:
1 | FROM ubuntu |
2 | ENTRYPOINT exec top -b |
当你运行这个景象,你会看到单独一个PID 1
进程:
1 | $ docker run -it --rm --name test top |
2 | Mem: 1704520K used, 352148K free, 0K shrd, 0K buff, 140368121167873K cached |
3 | CPU: 5% usr 0% sys 0% nic 94% idle 0% io 0% irq 0% sirq |
4 | Load average: 0.08 0.03 0.05 2/98 6 |
5 | PID PPID USER STAT VSZ %VSZ %CPU COMMAND |
6 | 1 0 root R 3164 0% 0% top -b |
这些会在docker stop
的时候干净利落的退出:
1 | $ /usr/bin/time docker stop test |
2 | test |
3 | real 0m 0.20s |
4 | user 0m 0.02s |
5 | sys 0m 0.04s |
如果你忘记在ENTRYPOINT的开头加上exec
:
1 | FROM ubuntu |
2 | ENTRYPOINT top -b |
3 | CMD --ignored-param1 |
你可以运行它(为了下一步的运行,需要给它个名字):
1 | $ docker run -it --name test top --ignored-param2 |
2 | Mem: 1704184K used, 352484K free, 0K shrd, 0K buff, 140621524238337K cached |
3 | CPU: 9% usr 2% sys 0% nic 88% idle 0% io 0% irq 0% sirq |
4 | Load average: 0.01 0.02 0.05 2/101 7 |
5 | PID PPID USER STAT VSZ %VSZ %CPU COMMAND |
6 | 1 0 root S 3168 0% 0% /bin/sh -c top -b cmd cmd2 |
7 | 7 1 root R 3164 0% 0% top -b |
你可以看到top
的输出信息中指定的ENTRYPOINT不是PID 1
。
如果你运行了docker stop test
,容器不会退出的很干净,stop
命令会在超时之后强制发送一个SIGKILL
:
1 | $ docker exec -it test ps aux |
2 | PID USER COMMAND |
3 | 1 root /bin/sh -c top -b cmd cmd2 |
4 | 7 root top -b |
5 | 8 root ps aux |
6 | $ /usr/bin/time docker stop test |
7 | test |
8 | real 0m 10.19s |
9 | user 0m 0.04s |
10 | sys 0m 0.03s |
理解CMD和ENTRYPOINT是如何交互的
CMD和ENTRYPOINT指令都定义了容器运行的时候命令是如何执行的,下面有几条规则描述了他们的关系:
- Dockerfile应当是定至少一条CMD或者ENTRYPOINT命令。
- 使用容器作为一个可执行文件的时候,ENTRYPOINT应当被定义。
- CMD应该作为一种为ENTRYPOINT命令定义默认参数的方法,或者作为容器中执行临时命令的方式。
- 在使用交互式参数运行容器的时候,CMD将会被重写。
下表展示了不同的ENTRYPOINT/CMD组合是如何执行的:
||No ENTRYPOINT|ENTRYPOINT exec_entry p1_entry|ENTRYPOINT [“exec_entry”, “p1_entry”]
-|-|-|-
No CMD|error,not allowed|/bin/sh -c exec_entry p1_entry|exec_entry p1_entry
CMD [“exec_cmd”, “p1_cmd”]|exec_cmd p1_cmd|/bin/sh -c exec_entry p1_entry|exec_entry p1_entry exec_cmd p1_cmd
CMD [“p1_cmd”, “p2_cmd”]|p1_cmd p2_cmd|/bin/sh -c exec_entry p1_entry|exec_entry p1_entry p1_cmd p2_cmd
CMD exec_cmd p1_cmd|/bin/sh -c exec_cmd p1_cmd|/bin/sh -c exec_entry p1_entry|exec_entry p1_entry /bin/sh -c exec_cmd p1_cmd
VOLUME
1 | VOLUME ["/data"] |
VOLUME指令创建了一个挂载点,并且指定了一个名字,然后标记这个挂载点来承载来自原生宿主机或者其他容器的额外被挂载数据卷。它的值可以使一个JSON数组,VOLUME ["/var/log/"]
,或者一个普通的有多个参数的字符串,比如VOLUME /var/log
或VOLUME /var/log /var/db
。想看更多的信息/举例和通过Docker客户端进行挂载的指令,可以参考Share Directories via Volumes文档。
docker run
命令用基础映像中指定位置存在的任何数据初始化新创建的卷。比如,思考下例Dockerfile片段:
1 | FROM ubuntu |
2 | RUN mkdir /myvol |
3 | RUN echo "hello world" > /myvol/greeting |
4 | VOLUME /myvol |
这个Dockerfile产生了一个镜像使docker run
创建了一个新的在/myvol
的挂载点而且复制greeting
文件到新创建的数据卷上。
注意指定数据卷
记住下列关于Dockerfile中关于数据卷的内容:
- 基于Windows的容器的数据卷:当你使用基于Windows的容器的时候,容器的数据卷的目标地址必须是下列之一:
- 一个不存在的或者是空的目录
- 除了
C:
的驱动
- 改变Dockerfile内的数据卷:如果任何构建步骤在被声明之前修改了数据卷内的数据,那么这些改变将被抛弃。
- JSON格式化:这个列表会被解析为一个JSON数组,所以你必须用双引号(“)包含字符,而不是用单引号(“)。
- 宿主目录是在docker运行时声明:宿主路径(挂载点),源于它自身的特性,依赖于宿主。这是为了保护镜像的可移植性,因为一个指定的宿主路径不能保证在所有主机上可得。基于这个原因,你不能在Dockerfile中挂载主机目录。VOLUMN指令不支持制定一个
host-dir
参数。你必须在创建或者运行容器的时候指定一个挂载点。
USER
1 | USER <user>[:<group>] or |
2 | USER <UID>[:<GID>] |
USER指令在使用CMD、RUN或者ENTRYPOINT命令启动一个镜像的时候,设置用户名(或者UID)并且可选择指定用户组(或者GID)。
注意: 当一个用户没有一个首选用户组的时候,镜像(或者下一个指令)将会用
root
用户组来运行。
在Windows下,必须先创建用户,如果不是內建用户的情况。这可以通过作为Dockerfile的一部分调用的net user命令来完成。
1 | FROM microsoft/windowsservercore |
2 | # Create Windows user in the container |
3 | RUN net user /add patrick |
4 | # Set it for subsequent commands |
5 | USER patrick |
WORKDIR
1 | WORKDIR /path/to/workdir |
WORKDIR指令为Dockerfile中后续的RUN、CMD、ENTRYPOINT、COPY和ADD指令设置工作路径。如果WORKDIR不存在,那么即便没使用Dockerfile中的任何指令,也会被创建。
WORKDIR指令会在Dockerfile中被多次使用。如果提供了一个相关的路径,那么它就会与前面的WORKDIR指令的路径相关联。比如:
1 | WORKDIR /a |
2 | WORKDIR b |
3 | WORKDIR c |
4 | RUN pwd |
最终pwd
命令的输出结果会是/a/b/c
。
WORKDIR指令可以处理之前使用ENV命令设置的环境变量。你可以只使用Dockerfile中明确设置的环境变量。比如:
1 | ENV DIRPATH /path |
2 | WORKDIR $DIRPATH/$DIRNAME |
3 | RUN pwd |
最终pwd
命令的输出结果会是/path/$DIRNAME
ARG
1 | ARG <name>[=<default value>] |
ARG指令定义了一个变量,用户可以使用--build-arg <varname> = <value>
标志在构建器中通过docker build
命令将其传递给构建器。如果用户指定了一个在Dockerfile中没有定义的构建参数,那么构建器会输出一个警告。
1 | [Warning] One or more build-args [foo] were not consumed. |
一个Dockerfile可以包含一个或者多个ARG指令,比如,下面是一个合法的Dockerfile:
1 | FROM busybox |
2 | ARG user1 |
3 | ARG buildno |
4 | ... |
注意: 不推荐使用构建阶段的变量传递密钥比如github的keys,用户证书等,构建阶段变量对任何用户都是可见的,只要使用
docker history
命令。
默认值
一个ARG指令可以选择包含一个默认值:
1 | FROM busybox |
2 | ARG user1=someuser |
3 | ARG buildno=1 |
4 | ... |
如果一个ARG指令有一个默认值,而且没有其他值在构建阶段传递,那么构建器就会使用默认值。
作用范围
一个ARG变量的定义从它被定义的哪一行开始生效,而不是从命令行或其他地方使用参数的时候开始。比如,考虑下面这个Dockerfile:
1 | 1 FROM busybox |
2 | 2 USER ${user:-some_user} |
3 | 3 ARG user |
4 | 4 USER $user |
5 | ... |
一个用户用下面的命令构建了这个文件:
1 | $ docker build --build-arg user=what_user . |
在这种情况下,RUN指令使用v1.0.0
而不是用户传递的ARG设置:v2.0.1
这种行为类似于一个shell脚本,其中一个本地作用域变量覆盖作为参数传递的变量或从环境继承的变量,定义点。
使用上面的示例,但使用不同的ENV规范,您可以在ARG和ENV指令之间创建更有用的交互:
1 | 1 FROM ubuntu |
2 | 2 ARG CONT_IMG_VER |
3 | 3 ENV CONT_IMG_VER ${CONT_IMG_VER:-v1.0.0} |
4 | 4 RUN echo $CONT_IMG_VER |
与ARG指令不通,ENV的值会在构建镜像的阶段一直存在。考虑一个docker不使用--build-arg
参数的构建:
1 | $ docker build . |
使用这个Dockerfile为例,CONT_IMG_VER
仍然在镜像中持续存在,但是它的值会是v1.0.0
,如同第三行中ENV指令设定的那样。
这个例子中的变量扩展技术允许你从命令行中传递一个参数,并通过利用ENV指令将其保存在最终镜像中。变量扩展只支持有限的Dockerfile指令。
预定义的ARGs
Docker 有一组预定义的ARG变量,您可以在Dockerfile中使用没有相应的ARG指令的变量。
HTTP_PROXY
http_proxy
HTTPS_PROXY
https_proxy
FTP_PROXY
ftp_proxy
NO_PROXY
no_proxy
想要使用它们,只需要使用命令行传递它们:
1 | --build-arg <varname>=<value> |
默认的,这些预定义变量会被docker history
的输出结果排除。将它们排除在外可以减少在HTTP_PROXY变量中意外泄漏敏感身份验证信息的风险。
举个栗子,考虑使用下面的语句构建Dockerfile:
1 | --build-arg HTTP_PROXY=http://user:pass@proxy.lon.example.com |
1 | FROM ubuntu |
2 | RUN echo "Hello World" |
在这种情况下,HTTP_PROXY
变量的值在docker history
中不可见,而且没有被缓存。如果你本地作了修改,而且你的代理服务器设置成了http://user:pass@proxy.sfo.example.com
,后续的构建不会导致缓存未命中。
如果你需要重写这种状况,你可能需要在Dockerfile中添加一个ARG声明,如下:
1 | FROM ubuntu |
2 | ARG HTTP_PROXY |
3 | RUN echo "Hello World" |
当构建这个Dockerfile的时候,HTTP_PROXY
被保存在了docker history
中,而且改变了它的值使构建缓存失效。
对构建缓存的影响
作为ENV变量,ARG变量不会保留在构建的镜像中。但是,ARG变量确实会以类似的方式影响构建缓存。如果Dockerfile定义了一个ARG变量,其值与以前的版本不同,那么在第一次使用时会发生“缓存未命中”,而不是其定义。尤其是,ARG指令之后的所有RUN指令隐式使用ARG变量(作为环境变量),因此可能导致缓存未命中。除非在Dockerfile中存在匹配的ARG语句,否则所有预定义的ARG变量都可以免于缓存。
举个栗子,考虑下面两个Dockerfile:
1 | 1 FROM ubuntu |
2 | 2 ARG CONT_IMG_VER |
3 | 3 RUN echo $CONT_IMG_VER |
1 | 1 FROM ubuntu |
2 | 2 ARG CONT_IMG_VER |
3 | 3 RUN echo hello |
如果在命令行中指定--build-arg CONT_IMG_VER = <value>
,则在这两种情况下,第2行的规范不会导致缓存未命中;第3行确实导致缓存未命中.ARG CONT_IMG_VER
导致RUN行被识别为与运行CONT_IMG_VER = <value>
echo hello相同,因此如果<value>
发生更改,则会导致缓存未命中。
考虑同一个命令行下的另一个例子:
1 | 1 FROM ubuntu |
2 | 2 ARG CONT_IMG_VER |
3 | 3 ENV CONT_IMG_VER $CONT_IMG_VER |
4 | 4 RUN echo $CONT_IMG_VER |
在这个例子中,高速缓存未命中发生在第3行。发生未命中是因为ENV中的变量值引用了ARG变量,并且该变量通过命令行进行了更改。在这个例子中,ENV命令会使镜像包含该值。
如果ENV指令覆盖同名的ARG指令,就像这个Dockerfile:
1 | 1 FROM ubuntu |
2 | 2 ARG CONT_IMG_VER |
3 | 3 ENV CONT_IMG_VER hello |
4 | 4 RUN echo $CONT_IMG_VER |
第3行不会导致缓存未命中,因为CONT_IMG_VER
的值是常量(hello
)。因此,RUN(第4行)上使用的环境变量和值在构建之间不会改变。
ONBUILD
1 | ONBUILD [INSTRUCTION] |
ONBUILD指令为镜像添加一个触发器指令,稍后将该镜像用作另一个构建的基础。触发器将在下游构建的上下文中执行,就像它已经在下游Dockerfile中的FROM指令之后立即插入一样。
任何构建指令都可以注册为触发器。
如果您正在构建将用作构建其他映像的基础的映像,那么这非常有用,例如可以使用用户特定配置自定义的应用程序构建环境或守护程序。
例如,如果您的映像是可重用的Python应用程序构建器,则需要将应用程序源代码添加到特定目录中,并且可能需要在此之后调用构建脚本。你现在不能只调用ADD和RUN,因为你还没有访问应用程序的源代码,每个应用程序的版本都不一样。您可以简单地向应用程序开发人员提供样板化的Dockerfile以复制粘贴到他们的应用程序中,但效率低下,容易出错,难以更新,因为它与特定于应用程序的代码混合在一起。
解决方案是使用ONBUILD注册先行指令,以便在以后的构建阶段运行。
这是它如何工作的:
- 当遇到ONBUILD指令时,构建器会为正在构建的映像的元数据添加一个触发器。该指令不会影响当前的构建。
- 在构建结束时,所有触发器的列表都存储在图像清单中的OnBuild键下。可以使用
docker inspect
命令检查它们。 - 稍后,可以使用FROM指令将镜像用作新构建的基础。作为处理FROM指令的一部分,下游构建器会查找ONBUILD触发器,并按照它们所注册的相同顺序执行它们。如果任何触发器失败,则FROM指令将被中止,从而导致构建失败。如果所有触发器都成功,则FROM指令完成,构建将像往常一样继续。
- 触发器在执行后从最终图像中清除。换句话说,他们不是由“大孩子”构建的遗传。
例如,你可能会添加这样的东西:
1 | [...] |
2 | ONBUILD ADD . /app/src |
3 | ONBUILD RUN /usr/local/bin/python-build --dir /app/src |
4 | [...] |
注意: 不允许使用
ONBUILD ONBUILD
链接ONBUILD
。
注意:
ONBUILD
指令可能不会触发FROM
或者MAINTAINER
指令。
STOPSIGNAL
1 | STOPSIGNAL signal |
STOPSIGNAL指令设置将被发送到容器的系统调用信号以退出。该信号可以是一个有效的无符号数字,与内核的syscall表中的一个位置(例如9)匹配,或者是一个SIGNAME格式的信号名称,例如SIGKILL。
HEALTHCHECK
HEALTHCHECK指令有两种形式:
HEALTHCHECK [选项] CMD命令
(通过在容器中运行一个命令来检查容器的健康状况)HEALTHCHECK NONE
(禁用从基础映像继承的任何健康检查)
HEALTHCHECK指令告诉Docker如何测试一个容器来检查它是否还在工作。这可以检测到一些情况,例如一个陷入无限循环的web服务器,即使服务器进程仍在运行,也无法处理新的连接。
当容器指定了健康状况检查时,除了正常状态之外,还有一个健康状态。这个状态是最初开始的。每当健康检查通过,它变得健康(无论以前在哪个状态)。经过一定次数的连续失败后,变得不健康。
可以在CMD之前出现的选项是:
--interval = DURATION
(默认:30秒)--timeout = DURATION
(默认:30s)--start-period = DURATION
(默认值:0s)--retries = N
(默认值:3)
运行状况检查将首先在容器启动后的间隔秒内运行,然后在每次前一次检查完成后再次间隔几秒。
如果单次运行检查花费的时间超过了超过秒数,则认为检查失败。
多次连续的健康检查失败的容器被认为是不健康的。
启动周期为需要时间启动的容器提供初始化时间。在此期间的探测失败不会计入最大重试次数。但是,如果在启动期间运行状况检查成功,则认为容器已启动,并且所有连续的故障都将计入最大重试次数。
Dockerfile中只能有一个HEALTHCHECK指令。如果您列出多个,则只有最后一个HEALTHCHECK将生效。
CMD关键字之后的命令可以是shell命令(例如,HEALTHCHECK CMD / bin / check-running)或exec阵列(与其他Dockerfile命令一样;例如参见ENTRYPOINT以获得详细信息)。
该命令的退出状态表示容器的健康状态。可能的值是:
- 0:成功 - 容器很健康而且随时可以使用
- 1:不健康 - 容器运行状态不正常
- 2:保留 - 不要使用这个退出状态码
例如,要每隔五分钟检查一次,网络服务器是否能够在三秒内为网站的主页提供服务:
1 | HEALTHCHECK --interval = 5m --timeout = 3s \ |
2 | CMD curl -f http:// localhost / ||exit 1 |
为了帮助调试失败的探测器,命令在stdout或stderr上写入的任何输出文本(UTF-8编码)都将存储在健康状态中,并且可以通过docker检查进行查询。这样的输出应该保持简短(目前只有前4096个字节被存储)。
当容器的运行状况发生变化时,会以新状态生成一个health_status
事件。
在Docker 1.12中添加了HEALTHCHECK功能。
SHELL
1 | SHELL ["executable", "parameters"] |
SHELL指令允许覆盖用于shell命令形式的默认shell。 Linux上的默认shell是["/bin/sh","-c"]
,在Windows上是["cmd","/S","/C"]
。 SHELL指令必须以JSON格式写入Dockerfile中。
SHELL指令在Windows中有两个常用和完全不同的本机shell特别有用:cmd和powershell,以及可用的备用shell,包括sh。
SHELL指令可以出现多次。每个SHELL指令将覆盖所有先前的SHELL指令,并影响所有后续指令。例如:
1 | FROM microsoft/windowsservercore |
2 | |
3 | # Executed as cmd /S /C echo default |
4 | RUN echo default |
5 | |
6 | # Executed as cmd /S /C powershell -command Write-Host default |
7 | RUN powershell -command Write-Host default |
8 | |
9 | # Executed as powershell -command Write-Host hello |
10 | SHELL ["powershell", "-command"] |
11 | RUN Write-Host hello |
12 | |
13 | # Executed as cmd /S /C echo hello |
14 | SHELL ["cmd", "/S"", "/C"] |
15 | RUN echo hello |
如果在Dockerfile中使用SHELL指令,则会影响以下指令:RUN,CMD和ENTRYPOINT。
以下示例是在Windows上可以通过使用SHELL指令简化的常见模式:
1 | ... |
2 | RUN powershell -command Execute-MyCmdlet -param1 "c:\foo.txt" |
3 | ... |
docker调用的命令是:
1 | cmd /S /C powershell -command Execute-MyCmdlet -param1 "c:\foo.txt" |
这是低效率的,原因有两个。首先,调用一个不必要的cmd.exe命令处理器(又名shell)。其次,shell格式中的每条RUN指令都需要在命令前加上一个额外的powershell命令。
为了提高效率,可以采用两种机制之一。一种是使用RUN命令的JSON格式,例如:
1 | ... |
2 | RUN ["powershell","-command","Execute-MyCmdlet","-param1 \"c:\\foo.txt\""] |
3 | ... |
虽然JSON格式是明确的,并且不使用不必要的cmd.exe,但是通过双引号和转义确实需要更多的冗长。另一种机制是使用SHELL指令和shell形式,为Windows用户提供更自然的语法,特别是与escape
转义指令结合使用时:
1 | # escape=` |
2 | |
3 | FROM microsoft/nanoserver |
4 | SHELL ["powershell","-command"] |
5 | RUN New-Item -ItemType Directory C:\Example |
6 | ADD Execute-MyCmdlet.ps1 c:\example\ |
7 | RUN c:\example\Execute-MyCmdlet -sample 'hello world' |
结果是:
1 | PS E:\docker\build\shell> docker build -t shell . |
2 | Sending build context to Docker daemon 4.096 kB |
3 | Step 1/5 : FROM microsoft/nanoserver |
4 | ---> 22738ff49c6d |
5 | Step 2/5 : SHELL powershell -command |
6 | ---> Running in 6fcdb6855ae2 |
7 | ---> 6331462d4300 |
8 | Removing intermediate container 6fcdb6855ae2 |
9 | Step 3/5 : RUN New-Item -ItemType Directory C:\Example |
10 | ---> Running in d0eef8386e97 |
11 | |
12 | |
13 | Directory: C:\ |
14 | |
15 | |
16 | Mode LastWriteTime Length Name |
17 | ---- ------------- ------ ---- |
18 | d----- 10/28/2016 11:26 AM Example |
19 | |
20 | |
21 | ---> 3f2fbf1395d9 |
22 | Removing intermediate container d0eef8386e97 |
23 | Step 4/5 : ADD Execute-MyCmdlet.ps1 c:\example\ |
24 | ---> a955b2621c31 |
25 | Removing intermediate container b825593d39fc |
26 | Step 5/5 : RUN c:\example\Execute-MyCmdlet 'hello world' |
27 | ---> Running in be6d8e63fe75 |
28 | hello world |
29 | ---> 8e559e9bf424 |
30 | Removing intermediate container be6d8e63fe75 |
31 | Successfully built 8e559e9bf424 |
32 | PS E:\docker\build\shell> |
SHELL指令也可以用来修改shell的运行方式。例如,在Windows上使用SHELL cmd /S /C /V:ON|OFF
,可以修改延迟的环境变量扩展语义。
SHELL指令也可以在Linux上使用,如果需要备用shell,如zsh,csh,tcsh等。
SHELL功能是在Docker 1.12中添加的。
Dockerfile 示例
在你可以看到Dockerfile语法之前,如果你对更多的实例感兴趣,可以参考docker文档。
1 | # Nginx |
2 | # |
3 | # VERSION 0.0.1 |
4 | |
5 | FROM ubuntu |
6 | LABEL Description="This image is used to start the foobar executable" Vendor="ACME Products" Version="1.0" |
7 | RUN apt-get update && apt-get install -y inotify-tools nginx apache2 openssh-server |
1 | # Firefox over VNC |
2 | # |
3 | # VERSION 0.3 |
4 | |
5 | FROM ubuntu |
6 | |
7 | # Install vnc, xvfb in order to create a 'fake' display and firefox |
8 | RUN apt-get update && apt-get install -y x11vnc xvfb firefox |
9 | RUN mkdir ~/.vnc |
10 | # Setup a password |
11 | RUN x11vnc -storepasswd 1234 ~/.vnc/passwd |
12 | # Autostart firefox (might not be the best way, but it does the trick) |
13 | RUN bash -c 'echo "firefox" >> /.bashrc' |
14 | |
15 | EXPOSE 5900 |
16 | CMD ["x11vnc", "-forever", "-usepw", "-create"] |
17 | # Multiple images example |
18 | # |
19 | # VERSION 0.1 |
20 | |
21 | FROM ubuntu |
22 | RUN echo foo > bar |
23 | # Will output something like ===> 907ad6c2736f |
24 | |
25 | FROM ubuntu |
26 | RUN echo moo > oink |
27 | # Will output something like ===> 695d7793cbe4 |
28 | |
29 | # You'll now have two images, 907ad6c2736f with /bar, and 695d7793cbe4 with |
30 | # /oink. |
Dockerfile 最佳实践
Docker可以通过从Dockerfile中阅读
一般参考和建议
容器应该是短暂的
你Dockerfile中定义的镜像所产生的容器应该是尽可能短暂的。提到“短暂“,我们的意思是它可以停止和被销毁,而且可以用一个最小化的启动和配置流程来构建一个新的替代它。
使用.dockerignore文件
当您发出docker build命令时,您所在的当前工作目录称为构建上下文,并且Dockerfile必须位于此构建上下文中的某个位置。默认情况下,它假定位于当前目录中,但是可以使用-f
标志指定不同的位置。无论Dockerfile实际存在于哪里,当前目录中的所有文件和目录的递归内容都会作为构建上下文发送到Docker守护进程。无意中包含构建映像所不需要的文件会产生较大的构建上下文和较大的映像大小。这些反过来可以增加构建时间,拉和推图像的时间以及容器的运行时间大小。要查看构建上下文有多大,在构建Dockerfile时查找如下消息。
1 | Sending build context to Docker daemon 187.8MB |
要排除与构建无关的文件,而不重构源代码库,请使用.dockerignore文件。该文件支持类似于.gitignore文件的排除模式。有关创建一个的信息,请参阅.dockerignore文件。除了使用.dockerignore文件之外,请查看以下关于多阶段构建的信息。
使用多阶段构建
如果你使用Docker 17.05或者更高版本,你可以使用多阶段构建来彻底减少最终镜像的大小。