dockerfile详解

Dockerfile是由一系列命令和参数构成的脚本,一个Dockerfile里面包含了构建整个image的完整命令。Docker通过docker build执行Dockerfile中的一系列命令自动构建image

用法

dockerfile 使用 docker build 命令来生成 image 镜像, docker build的语法如下:

1
Usage:	docker build [OPTIONS] PATH | URL | -
  • PATH 是本地文件路径
  • URL 是 git repository 的路径

构建镜像时, docker 将会递归地读取路径下的所有文件. 因此, PATH包括任何子目录,URL包括repository及submodules

官方推荐: 在构建镜像的时候, 最好使用干净的目录进行构建, 减少不必要的索引性能损耗, 提高构建效率

如果在特殊环境的限制下, 一定要在有众多干扰文件的目录下构建镜像, 可以使用 .dockerignore 文件来屏蔽索引这些文件

你可以使用如下方式进行构建

1
2
3
4
5
6
7
8
9
10
11
# 最简单的执行方式
$ docker build .

# 如果文件名不是dockerfile, 那么需要显示的精确指定文件
$ docker build -f /path/to/a/Dockerfile .

# 顺便打个标签🏷
$ docker build -t shykes/myapp .

# 打多个标签
$ docker build -t shykes/myapp:1.0.2 -t shykes/myapp:latest .

只要有可能,Docker将重新使用中间images(缓存),以显着加速docker build过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ docker build -t svendowideit/ambassador .
Sending build context to Docker daemon 15.36 kB
Step 1 : FROM alpine:3.2
---> 31f630c65071
Step 2 : MAINTAINER SvenDowideit@home.org.au
---> Using cache
---> 2a1c91448f5f
Step 3 : RUN apk update && apk add socat && rm -r /var/cache/
---> Using cache
---> 21ed6e7fbb73
Step 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

构建成功后,就可以准备Pushing a repository to its registry

Format 语法格式

Dockerfile的格式如下:

1
2
# Comment
INSTRUCTION arguments
  • Line1 第一行是解析器指令
  • INSTRUCTION 是不区分大小写的,不过建议大写. 这里写 dockerfile 中支持的指令集
  • arguments 这里写对应的命令
  • dockerfile 中的第一个指令必须是FORM 指定构建镜像的Base Image, 这个 Base Image 基础镜像可以是一个系统的镜像比如CentOS Ubuntu等, 也可以是一个应用镜像, 用来做二次定制, 比如 nginx httpd mysql
  • dockerfile中以#开头的行都将视为注释,除非是 Parser directives 解析器指令, 而且 dockerfile 中不支持连续注释
1
2
# Comment
RUN echo 'we are running some # of cool things'

ParserDirectives 解析器指令

解析器指令是可选的, 并且影响处理 dockerfile 中后续的执行方式. 解析器的指令不会向构建中添加图层, 并且不会显示在构建的步骤当中. 解析器的指令是以 # directive = value 的形式写成一种特殊类型的注释, 且单个指令只能使用一次.(到目前为止 2017/04/19 dockerfile 只支持一种解析器指令)
当 docker 遇到普通注释行或空行或是已经被执行过的解析器指令, docker 都不会再寻找任何解析器的指令. 因此, 所有的解析器指令都必须位于 dockerfile 的最顶端

1
2
3
4
# escape=`

FROM windowsservercore
...

目前解析器指令只支持:

  • escape

escape

escape 作为 dockerfile 解析器目前为止唯一支持的指令, 目前有以下两种用法:

1
2
3
# escape=\ (backslash)
或者
# escape=` (backtick)

escape 的作用是声明 dockerfile 中的转义符, 在不指定的默认情况下, 转义符是反斜杠 \ 但是如果是在 Windows 中, 反斜杠是正常的路径分隔符, 不属于转义的范畴, 这种情况下就需要使用两个反斜杠 \\ 来表示一个路径中的反斜杠 \

转义字符既用于转义行中的字符,也用于转义换行符. 这允许Dockerfile指令跨越多行. 注意,不管escape解析器指令是否包括在Dockerfile中,在RUN命令中不执行转义,除非在行的末尾, 用来转义换行符

请考虑以下示例,这将在Windows上以非显而易见的方式失败。第二行末尾的第二个\将被解释为换行符,而不是从第一个\转义的目标。类似地,假设第三行结尾处的\实际上作为一条指令处理,它将被视为行继续。这个dockerfile的结果是第二行和第三行被认为是单个指令:

1
2
3
4
5
6
7
8
9
10
11
12
FROM windowsservercore
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 : FROM windowsservercore
---> dbfee88ee9fd
Step 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上的所有命令支持/作为路径分隔符.

通过添加转义解析器指令,下面的Dockerfile在Windows上使用文件路径的自然平台语义成功:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# escape=`

FROM windowsservercore
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 : FROM windowsservercore
---> dbfee88ee9fd
Step 2 : COPY testfile.txt c:\
---> 99ceb62e90df
Removing intermediate container 62afbe726221
Step 3 : RUN dir c:\
---> Running in a5ff53ad6323
Volume in drive C has no label.
Volume Serial Number is 1440-27FA

Directory of c:\

03/25/2016 05:28 AM <DIR> inetpub
03/25/2016 04:22 AM <DIR> PerfLogs
04/22/2016 10:59 PM <DIR> Program Files
03/25/2016 04:22 AM <DIR> Program Files (x86)
04/18/2016 09:26 AM 4 testfile.txt
04/22/2016 10:59 PM <DIR> Users
04/22/2016 10:59 PM <DIR> Windows
1 File(s) 4 bytes
6 Dir(s) 21,252,689,920 bytes free
---> 2569aa19abef
Removing intermediate container a5ff53ad6323
Successfully built 2569aa19abef
PS C:\John>

ENV 环境变量替换

环境变量使用ENV命令声明, 使用$variable_name${variable_name}来调用, 带大括号的语法是被用来解决不带空格的变量名字问题, 例如:${foo}_bar

${variable_name}语法还支持以下指定的一些标准bash修饰符:

  • ${variable:-word}表示如果设置了variable, 则结果将是该值; 如果variable未设置, 那么word将是结果
  • ${variable:+word}表示如果设置了variable, 那么word将是结果, 否则结果是空字符串

在所有情况下, word可以是任何字符串, 包括额外的环境变量

可以通过在变量之前添加\来转义: \$foo\${foo}, 分别转换为$foo${foo}

示例(解析的表示显示在#后面):

1
2
3
4
5
FROM busybox
ENV foo /bar
WORKDIR ${foo} # WORKDIR /bar
ADD . $foo # ADD . /bar
COPY \$foo /quux # COPY $foo /quux

Dockerfile中的以下指令列表支持环境变量:

  • ADD
  • COPY
  • ENV
  • EXPOSE
  • LABEL
  • USER
  • WORKDIR
  • VOLUME
  • STOPSIGNAL

以及:

  • ONBUILD(当与上面支持的指令之一组合时)

注意: 在1.4之前, ONBUILD指令不支持环境变量, 即使与上面列出的任何指令相结合

ENV的使用中, 要特别注意以下情况:

1
2
3
ENV abc=hello
ENV abc=bye def=$abc
ENV ghi=$abc

最终的结果为:

  • def 的值为 hello
  • ghi 的值为 bye

为什么def的值不是bye呢? 因为第一行设置abc变量的值为hello后, 到第二行执行的时候, 首先进行变量的渲染(替换), 然后再执行环境变量的设置

.dockerignore

docker执行构建后, 会首先去根目录中查找.dockerignore文件, 由于在构建的时候, docker 会递归索引当前目录下的所有文件, 为了避免索引不必要的目录, 可以使用.dockerignore文件声明不需要索引的目录

  • # 注释
  • * 任意多个字符
  • ? 任意一个字符
  • ! 异常模式, 用于排除例外
1
2
3
4
# comment
*/temp*
*/*/temp*
temp?
1
2
*.md
!README.md

FORM

1
2
3
4
5
FROM <image>
# 或则
FROM <image>:<tag>
# 或则
FROM <image>@<digest>

FROM指令为后续指令设置Base Image. 因此, 有效的Dockerfile必须具有FROM作为其第一条指令. image可以是任何有效的image, 可以从镜像仓库中拉取镜像.

  • FROM必须是Dockerfile中的第一个非注释指令
  • FROM可以在单个Dockerfile中多次出现,以创建多个镜像。每个新的FROM命令之前会输出最一个image ID
  • tag或digest是可选的. 如果省略其中任何一个, 构建器将默认使用latest. 如果构建器与tag值不匹配, 则构建器将返回错误

RUN

RUN 指令有两种运行方式:

  • RUN (shell form, the command is run in a shell, which by default is /bin/sh -c on Linux or cmd /S /C on Windows)
  • RUN [“executable”, “param1”, “param2”] (exec form)

RUN指令将在当前image之上的新层中执行任何命令, 并提交结果. 生成的已提交image将用于Dockerfile中的下一步

exec形式使得可以避免shell字符串变化, 以及使用不包含指定的shell可执行文件的基本image来运行RUN命令

在shell形式中,可以使用\(反斜杠)将单个RUN指令继续到下一行, 这也是大部分 dockerfile 使用的形式

RUN命令的使用中, 需要注意一下几点:

  • 要使用不同的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"]
  • 在JSON形式中, 有必要转义反斜杠. 这在Windows上特别相关, 其中反斜杠是路径分隔符. 因为不是有效的JSON, 并且以意外的方式失败, 以下行将被视为shell形式: RUN ["c:\windows\system32\tasklist.exe"] 此示例的正确语法为: RUN ["c:\\windows\\system32\\tasklist.exe"]

用于RUN指令的高速缓存在下一次构建期间不会自动失效. 用于诸如RUN apt-get dist-upgrade之类的指令的高速缓存将在下一次构建期间被重用. 可以通过使用--no-cache标志来使用于RUN指令的高速缓存无效, 例如 docker build --no-cache

Known issues(RUN

Issue 783是关于在使用AUFS文件系统时可能发生的文件权限问题。例如,您可能会在尝试rm文件时注意到它。对于具有最近aufs版本的系统(即,可以设置dirperm1安装选项),docker将尝试通过使用dirperm1选项安装image来自动解决问题。

MAINTAINER 作者信息(已废弃)

1
MAINTAINER <name>

MAINTAINER指令允许您设置生成的images的作者字段

在新的 docker 版本中, 该指令将被LABEL替代

1
LABEL maintainer "SvenDowideit@home.org.au"

CMD

CMD指令三种形式:

  • CMD [“executable”,”param1”,”param2”] (exec格式, 首选形式)
  • CMD [“param1”,”param2”] (作为 ENTRYPOINT 指令的默认参数)
  • CMD command param1 param2 (shell 格式)

在Dockerfile中只能有一个CMD指令. 如果列出了多个CMD, 则只有最后一个CMD生效

CMD的主要目的是为执行容器提供默认值. 这些默认值可以包括可执行文件, 或者它们可以省略可执行文件, 在这种情况下, 你还必须指定ENTRYPOINT指令

当以shell或exec格式使用时,CMD指令设置运行image时要执行的命令

如果使用CMD的shell形式,那么将在/bin/sh -c中执行:

1
2
FROM ubuntu
CMD echo "This is a test." | wc -

如果你想运行你的没有shell,那么你必须将该命令表示为一个JSON数组,并给出可执行文件的完整路径。此数组形式是CMD的首选格式。任何其他参数必须单独表示为数组中的字符串(注意使用双引号):

1
2
FROM ubuntu
CMD ["/usr/bin/wc","--help"]

如果你希望你的容器每次运行相同的可执行文件,那么你应该考虑使用ENTRYPOINT结合CMD

如果用户指定docker run参数,那么它们将覆盖CMD中指定的默认值

注意:

  • 不要将RUN和CMD混淆. RUN实际上运行一个命令并提交结果;CMD在构建时不执行任何操作, 但指定了image的预期命令
  • 如果使用CMD为ENTRYPOINT指令提供默认参数, CMD和ENTRYPOINT指令都应以JSON数组格式指定
  • exec形式作为JSON数组解析, 这意味着您必须在单词之外使用双引号"而不是单引号'
  • 与shell表单不同, exec表单不调用命令shell. 这意味着正常的shell处理不会发生. 例如, CMD ["echo","$HOME"] 不会在$HOME上进行可变替换. 如果你想要shell处理, 那么使用shell形式或直接执行一个shell, 例如: CMD ["sh","-c","echo $HOME"]

LABEL

1
2
3
4
5
LABEL <key>=<value> <key>=<value> <key>=<value> ...

LABEL <key>=<value>
LABEL <key>=<value>
LABEL <key>=<value>

LABEL 指令向image添加元数据. LABEL是键值对. 要在LABEL值中包含空格, 需要使用引号和反斜杠, 就像在命令行解析中一样

1
2
3
4
5
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."

一个image可以有多个label. 要指定多个label, Docker建议在可能的情况下将标签合并到单个LABEL指令中. 每个LABEL指令产生一个新层, 如果使用许多标签, 可能会导致镜像的效率低下. 该示例产生单个镜像层

1
LABEL multi.label1="value1" multi.label2="value2" other="value3"

上面的也可写为:

1
2
3
LABEL multi.label1="value1" \
multi.label2="value2" \
other="value3"

如果Docker遇到已经存在的label/key,则新值将覆盖具有相同键的任何先前标签

EXPOSE

1
EXPOSE <port> [<port>...]

EXPOSE指令通知Docker容器在运行时监听指定的网络端口. EXPOSE不使主机的容器的端口可访问, 为此, 必须使用-p标志发布一系列端口, 或者使用-P标志发布所有暴露的端口。可以公开一个端口号,并用另一个端口号在外部发布

这里指定的所有端口都是容器内部声明容器运行后要监听的端口, 不能在这里定义宿主机与容器之间端口的对应关系! 比如一个 nginx 的镜像, 内部监听80端口, 在容器运行之后, 你想映射到8080端口, 此时在 dockerfile 中写 8080:80 是没有任何意义的, 镜像(dockerfile)里的声明的端口与容器运行指定的端口是分开的, 不能在 dockerfile 中指定未来容器运行时映射的端口

ENV

1
2
ENV <key> <value>
ENV <key>=<value> ... (首选形式)
  • 第一种形式只能设置一个环境变量
  • 第二种形式可以在一个高速缓存层中设置多个变量, 即使是一个变量要设置也推荐使用这种模式来设置
1
2
3
4
5
6
ENV myName="John Doe" myDog=Rex\ The\ Dog \
myCat=fluffy
# 和
ENV myName John Doe
ENV myDog Rex The Dog
ENV myCat fluffy

将在最终容器中产生相同的结果,但第一种形式是优选的,因为它产生单个高速缓存层

ADD

两种形式:

1
2
ADD <src>... <dest>
ADD ["<src>",... "<dest>"] (对于包含空格的路径,此形式是必需的)

ADD 指令从复制新文件, 目录或远程文件URL, 并将它们添加到容器的文件系统, 路径

可以指定多个资源, 但如果它们是文件或目录, 那么它们必须是相对于正在构建的源目录

每个可能包含通配符, 匹配将使用Go的filepath.Match规则完成. 例如:

1
2
ADD hom* /mydir/        # adds all files starting with "hom"
ADD hom?.txt /mydir/ # ? is replaced with any single character, e.g., "home.txt"

是绝对路径或相对于WORKDIR的路径, 源将在目标容器中复制到其中

1
2
ADD test relativeDir/          # adds "test" to `WORKDIR`/relativeDir/
ADD test /absoluteDir/ # adds "test" to /absoluteDir/

所有新文件和目录都使用UID和GID为0创建

ADD遵守以下规则:

  • 路径必须在构建的上下文中, 不能ADD ../something /something 因为docker构建的第一步是发送上下文目录(和子目录)到docker守护进程.

  • 如果是URL并且不以尾部斜杠结尾,则从URL下载文件并将其复制到

  • 如果是URL并且以尾部斜杠结尾,则从URL中推断文件名,并将文件下载到/ 例如, ADD http://example.com/foobar /会创建文件/foobar 网址必须有一个非普通的路径, 以便在这种情况下可以发现一个适当的文件名http://example.com不会工作

  • 如果是目录, 则复制目录的整个内容, 包括文件系统元数据
    注意:目录本身不被复制,只是其内容

  • 如果是识别的压缩格式(identity,gzip,bzip2或xz)的本地tar存档,则将其解包为目录. 来自远程URL的资源不会解压缩. 当目录被复制或解压缩时, 它具有与tar -x相同的行为
    注意:文件是否被识别为识别的压缩格式, 仅基于文件的内容, 而不是文件的名称. 例如, 如果一个空文件以.tar.gz结尾, 则不会被识别为压缩文件, 并且不会生成任何解压缩错误消息, 而是将该文件简单地复制到目的地

  • 如果是任何其他类型的文件, 它会与其元数据一起单独复制. 在这种情况下, 如果以尾部斜杠/结尾,它将被认为是一个目录, 并且的内容将被写在/base()

  • 如果直接或由于使用通配符指定了多个资源, 则必须是目录, 并且必须以斜杠/结尾

  • 如果不以尾部斜杠结尾,它将被视为常规文件,的内容将写在

  • 如果不存在, 则会与其路径中的所有缺少的目录一起创建

COPY

两种形式:

1
2
COPY <src>... <dest>
COPY ["<src>",... "<dest>"] (this form is required for paths containing whitespace)

基本和ADD类似,不过COPY的不能为URL

与 ADD 不同的一点是, 遇到压缩文件, COPY 不会去检测文件的压缩属性, 不会做任何的解压缩操作, 会直接把源文件原封不动的拷贝到目标地址

ENTRYPOINT

两种形式:

1
2
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或运行命令行参数, 但是缺点是ENTRYPOINT将作/bin/sh -c的子命令启动, 它不传递信号. 这意味着可执行文件将不是容器的PID 1, 并且不会接收Unix信号, 因此你的可执行文件将不会从docker stop <container>接收到SIGTERM

只有Dockerfile中最后一个ENTRYPOINT指令会有效果

Exec form ENTRYPOINT example

可以使用ENTRYPOINT的exec形式设置相当稳定的默认命令和参数, 然后使用任一形式的CMD设置更可能更改的其他默认值

1
2
3
FROM ubuntu
ENTRYPOINT ["top", "-b"]
CMD ["-c"]

以下Dockerfile显示使用ENTRYPOINT在前台运行Apache, 作为PID 1

1
2
3
4
5
FROM debian:stable
RUN apt-get update && apt-get install -y --force-yes apache2
EXPOSE 80 443
VOLUME ["/var/www", "/var/log/apache2", "/etc/apache2"]
ENTRYPOINT ["/usr/sbin/apache2ctl", "-D", "FOREGROUND"]

如果需要为单个可执行文件编写启动脚本,可以使用exec和gosu命令确保最终可执行文件接收到Unix信号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/bash
set -e

if [ "$1" = 'postgres' ]; then
chown -R postgres "$PGDATA"

if [ -z "$(ls -A "$PGDATA")" ]; then
gosu postgres initdb
fi

exec gosu postgres "$@"
fi

exec "$@"
  • set -e : 在脚本的执行中任何一行出现执行失败都立即退出脚本
  • set -eo pipefail : 在管道的使用中, 任意一个环节出现问题都立即退出
  • set – postgres “$@” : 将后面的每一个值依次放入 $1 $2 $3 …
  • $@ : 传给脚本的所有参数的列表

上面脚本的意思是, 首先判断第一个参数是不是预期中的可执行命令, 如果第一个参数是预期中的可执行命令, 那么后面参数默认均为第一个(参数)命令的参数; 如果第一个参数不是预期中的可行性命令, 那么就认为这是一个全新的命令要执行, 所以使用exec $@完整地执行命令

如果需要在关闭时进行一些额外的清理(或与其他容器通信)或者协调多个可执行文件, 需要确保ENTRYPOINT脚本接收到Unix信号, 传递它们, 然后做一些更多的善后工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/bin/sh
# Note: I've written this using sh so it works in the busybox container too

# USE the trap if you need to also do manual cleanup after the service is stopped,
# or need to start multiple services in the one container
trap "echo TRAPed signal" HUP INT QUIT TERM

# start service in background here
/usr/sbin/apachectl start

echo "[hit enter key to exit] or run 'docker stop <container>'"
read

# stop service and clean up here
echo "stopping apache"
/usr/sbin/apachectl stop

echo "exited $0"

Shell form ENTRYPOINT example

你可以为ENTRYPOINT指定一个纯字符串,它将在/bin/sh -c中执行。这种形式将使用shell处理来替换shell环境变量,并且将忽略任何CMDdocker run命令行参数。要确保docker stop将正确地控制ENTRYPOINT可执行文件,必须用exec启动它:

1
2
FROM ubuntu
ENTRYPOINT exec top -b

运行此image时,您将看到单个PID 1进程:

1
2
3
4
5
6
$ docker run -it --rm --name test top
Mem: 1704520K used, 352148K free, 0K shrd, 0K buff, 140368121167873K cached
CPU: 5% usr 0% sys 0% nic 94% idle 0% io 0% irq 0% sirq
Load average: 0.08 0.03 0.05 2/98 6
PID PPID USER STAT VSZ %VSZ %CPU COMMAND
1 0 root R 3164 0% 0% top -b

使用docker stop可以干净的退出:

1
2
3
4
5
$ /usr/bin/time docker stop test
test
real 0m 0.20s
user 0m 0.02s
sys 0m 0.04s

如果忘记将exec添加到您的ENTRYPOINT的开头:

1
2
3
FROM ubuntu
ENTRYPOINT top -b
CMD --ignored-param1

然后,你可以运行它(给它一个名称为下一步):

1
2
3
4
5
6
7
$ docker run -it --name test top --ignored-param2
Mem: 1704184K used, 352484K free, 0K shrd, 0K buff, 140621524238337K cached
CPU: 9% usr 2% sys 0% nic 88% idle 0% io 0% irq 0% sirq
Load average: 0.01 0.02 0.05 2/101 7
PID PPID USER STAT VSZ %VSZ %CPU COMMAND
1 0 root S 3168 0% 0% /bin/sh -c top -b cmd cmd2
7 1 root R 3164 0% 0% top -b

您可以从top的输出中看到指定的ENTRYPOINT不是PID 1。

如果你运行docker停止测试,容器将不会完全退出, 超时后停止命令将强制发送SIGKILL:

1
2
3
4
5
6
7
8
9
10
$ docker exec -it test ps aux
PID USER COMMAND
1 root /bin/sh -c top -b cmd cmd2
7 root top -b
8 root ps aux
$ /usr/bin/time docker stop test
test
real 0m 10.19s
user 0m 0.04s
sys 0m 0.03s

Understand how CMD and ENTRYPOINT interact

CMDENTRYPOINT指令定义在运行容器时执行什么命令, 有如下使用规则

  • Dockerfile应该至少指定一个CMDENTRYPOINT命令
  • 当使用容器作为可执行文件时,应该定义ENTRYPOINT
  • CMD应该用作定义ENTRYPOINT命令的默认参数或在容器中执行ad-hoc命令的一种方法
  • 当运行带有替代参数的容器时,CMD将被覆盖

下表显示了对不同ENTRYPOINT/CMD组合执行的命令:

no ENTRYPOINT ENTRYPOINT exec_enty 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_cmd p1_cmd exec_entry p1_entry exec_cmd p1_cmd
CMD [“p1_cmd”, “p2_cmd”] p1_cmd p2_cmd /bin/sh -c exec_entry p1_entry p1_cmd p2_cmd 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 /bin/sh -c exec_cmd p1_cmd exec_entry p1_entry /bin/sh -c exec_cmd p1_cmd

VOLUME

1
VOLUME ["/data"]

VOLUME指令创建具有指定名称的挂载点,并将其标记为从本机主机或其他容器保留外部挂载的卷。该值可以是JSON数组VOLUME ["/var/log/"]或具有多个参数的纯字符串,例如VOLUME /var/logVOLUME /var/log /var/db

1
2
3
4
FROM ubuntu
RUN mkdir /myvol
RUN echo "hello world" > /myvol/greeting
VOLUME /myvol

USER

1
USER daemon

USER指令设置运行image时使用的用户名或UID, 以及Dockerfile中的任何RUN,CMDENTRYPOINT指令

WORKDIR

1
WORKDIR /path/to/workdir

WORKDIR指令为Dockerfile中的任何RUNCMDENTRYPOINTCOPYADD指令设置工作目录。如果WORKDIR不存在,它将被创建,即使它没有在任何后续的Dockerfile指令中使用

它可以在一个Dockerfile中多次使用. 如果提供了相对路径, 它将相对于先前WORKDIR指令的路径. 例如:

1
2
3
4
WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd

在这个Dockerfile中的最终pwd命令的输出是/a/b/c

WORKDIR指令可以解析先前使用ENV设置的环境变量。您只能使用在Dockerfile中显式设置的环境变量. 例如:

1
2
3
ENV DIRPATH /path
WORKDIR $DIRPATH/$DIRNAME
RUN pwd

pwd命令在该Dockerfile中输出的最后结果是/path/$DIRNAME

ARG

1
ARG <name>[=<default value>]

ARG指令定义一个变量, 用户可以使用docker build命令使用--build-arg <varname> = <value>标志,在构建时将其传递给构建器. 如果用户指定了一个未在Dockerfile中定义的构建参数, 构建将输出错误

1
One or more build-args were not consumed, failing build.

Dockerfile作者可以通过指定ARG一个或多个变量, 通过多次指定ARG来定义单个变量. 例如, 一个有效的Dockerfile

1
2
3
4
FROM busybox
ARG user1
ARG buildno
...

Dockerfile作者可以可选地指定ARG指令的默认值:

1
2
3
4
FROM busybox
ARG user1=someuser
ARG buildno=1
...

如果ARG值具有缺省值,并且如果在构建时没有传递值,则构建器使用缺省值。

ARG变量定义从在Dockerfile中定义的行开始生效,而不是从命令行或其他地方的参数使用。例如,考虑这个Dockerfile:

1
2
3
4
5
1 FROM busybox
2 USER ${user:-some_user}
3 ARG user
4 USER $user
...

用户构建此文件如下:

1
$ docker build --build-arg user=what_user Dockerfile

第2行的USER将评估为some_user,因为用户变量在后续行3上定义。第4行的USER在定义用户时估计为what_user,在命令行中传递what_user值。在通过ARG指令定义之前,变量的任何使用都将导致空字符串。

警告:不建议使用build-time变量来传递诸如github密钥, 用户凭证等密码. 构建时变量值使用docker history命令对镜像的任何用户可见

可以使用ARGENV指令来指定RUN指令可用的变量。使用ENV指令定义的环境变量总是覆盖同名的ARG指令。思考这个Dockerfile带有ENVARG指令。

1
2
3
4
1 FROM ubuntu
2 ARG CONT_IMG_VER
3 ENV CONT_IMG_VER v1.0.0
4 RUN echo $CONT_IMG_VER

然后,假设此image是使用此命令构建的:

1
$ docker build --build-arg CONT_IMG_VER=v2.0.1 Dockerfile

在这种情况下,RUN指令使用v1.0.0而不是用户传递的ARG设置:v2.0.1此行为类似于shell脚本,其中本地作用域变量覆盖作为参数传递或从环境继承的变量,从其定义点。

使用上述示例,但使用不同的ENV规范,您可以在ARGENV指令之间创建更有用的交互:

1
2
3
4
1 FROM ubuntu
2 ARG CONT_IMG_VER
3 ENV CONT_IMG_VER ${CONT_IMG_VER:-v1.0.0}
4 RUN echo $CONT_IMG_VER

ARG指令不同, ENV值始终保留在image中. 考虑一个没有-build-arg标志的docker构建:

1
$ docker build Dockerfile

使用这个Dockerfile示例,CONT_IMG_VER仍然保留在映像中,但它的值将是v1.0.0,因为它是ENV指令在第3行中的默认设置。

此示例中的变量扩展技术允许您从命令行传递参数,并通过利用ENV指令将它们持久保存在最终image中

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>

Impact on build caching

ARG变量不会持久化到构建的image中,因为ENV变量是。但是,ARG变量会以类似的方式影响构建缓存。如果一个Dockerfile定义一个ARG变量,它的值不同于以前的版本,那么在它的第一次使用时会出现一个“cache miss”,而不是它的定义。特别地,在ARG指令之后的所有RUN指令都隐式地使用ARG变量(作为环境变量),因此可能导致高速缓存未命中。

例如,考虑这两个Dockerfile:

1
2
3
1 FROM ubuntu
2 ARG CONT_IMG_VER
3 RUN echo $CONT_IMG_VER
1
2
3
1 FROM ubuntu
2 ARG CONT_IMG_VER
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
2
3
4
1 FROM ubuntu
2 ARG CONT_IMG_VER
3 ENV CONT_IMG_VER $CONT_IMG_VER
4 RUN echo $CONT_IMG_VER

在此示例中,高速缓存未命中发生在第3行。由于变量在ENV中的值引用ARG变量并且该变量通过命令行更改,因此发生了未命中。在此示例中,ENV命令使image包括该值。

如果ENV指令覆盖同名的ARG指令,就像这个Dockerfile:

1
2
3
4
1 FROM ubuntu
2 ARG CONT_IMG_VER
3 ENV CONT_IMG_VER hello
4 RUN echo $CONT_IMG_VER

第3行不会导致高速缓存未命中,因为CONT_IMG_VER的值是一个常量(hello)因此,RUN(第4行)上使用的环境变量和值在构建之间不会更改

ONBUILD

1
ONBUILD [INSTRUCTION]

ONBUILD指令在image被用作另一个构建的基础时,向image添加要在以后执行的trigger指令。trigger将在下游构建的上下文中执行,就好像它已经在下游Dockerfile中的1FROM1指令之后立即插入。

任何构建指令都可以注册为trigger。

如果您正在构建将用作构建其他image的基础的图像,例如应用程序构建环境或可以使用用户特定配置自定义的后台驻留程序,这将非常有用。

例如,如果您的image是可重用的Python应用程序构建器,则需要将应用程序源代码添加到特定目录中,并且可能需要在此之后调用构建脚本。你不能只是调用ADDRUN现在,因为你还没有访问应用程序源代码,它将是不同的每个应用程序构建。您可以简单地为应用程序开发人员提供一个样板Dockerfile以将其复制粘贴到其应用程序中,但这是低效,容易出错,并且很难更新,因为它与应用程序特定的代码混合。

解决方案是使用ONBUILD来注册提前指令,以便稍后在下一个构建阶段运行。

以下是它的工作原理:

  1. 当遇到ONBUILD指令时,构建器会向正在构建的image的元数据添加trigger。该指令不会另外影响当前构建。
  2. 在构建结束时,所有trigger的列表存储在image清单中的OnBuild键下。可以使用docker inspect命令检查它们。
  3. 稍后,可以使用FROM指令将image用作新构建的基础。作为处理FROM指令的一部分,下游构建器会查找ONBUILDtriggers,并按照它们注册的顺序执行它们。如果任何触发器失败,则FROM指令中止,这又导致构建失败。如果所有触发器都成功,则FROM指令完成并且构建如常继续。触发器在执行后从最终image中清除。换句话说,它们不会被“grand-children”构建继承。 例如,您可以添加如下: [...] ONBUILD ADD . /app/src ONBUILD RUN /usr/local/bin/python-build --dir /app/src [...]> 警告:不允许使用ONBUILD ONBUILD链接ONBUILD指令。 > 警告ONBUILD指令可能不会触发FROMMAINTAINER指令。

STOPSIGNAL

1
STOPSIGNAL signal

STOPSIGNAL指令设置将发送到容器以退出的系统调用信号。该信号可以是与内核系统调用表中的位置匹配的有效无符号数,例如9,或者是SIGNAME格式的信号名称,例如SIGKILL

HEALTHCHECK

两种形式:

  • HEALTHCHECK [OPTIONS] CMD command (通过在容器中运行命令来检查容器运行状况)
  • HEALTHCHECK NONE (禁用从基本映像继承的任何运行状况检查)

HEALTHCHECK指令告诉Docker如何测试容器以检查它是否仍在工作。这可以检测到诸如Web服务器被卡在无限循环中并且无法处理新连接的情况,即使服务器进程仍在运行。

当容器指定了healthcheck时,除了其正常状态外,它还具有健康状态。此状态最初开始。 每当健康检查通过,它变得健康(无论之前的状态)。在一定数量的连续故障后,它变得不健康。

在CMD之前可以出现的选项有:

  • --interval=DURATION (default: 30s)
  • --timeout=DURATION (default: 30s)
  • --retries=N (default: 3)

运行状况检查将首先在容器启动后运行interval秒,然后在每次上次检查完成后再次运行interval秒。

如果检查的单次运行所花费的时间超过timeout秒数,则该检查被认为已失败。

它需要retries连续的健康检查的故障,容器被认为是不健康的。

在Dockerfile中只能有一个HEALTHCHECK指令。如果您列出多个,则只有最后一个HEALTHCHECK将生效。

CMD关键字之后的命令可以是shell命令(例如HEALTHCHECK CMD /bin/check-running)或exec数组(如同其他Dockerfile命令一样;详情参见ENTRYPOINT)。

命令的退出状态表示容器的运行状况。 可能的值为:

  • 0: success - the container is healthy and ready for use
  • 1: unhealthy - the container is not working correctly
  • 2: reserved - do not use this exit code

例如,要每五分钟检查一次Web服务器能够在三秒钟内为网站的主页提供服务:

1
2
HEALTHCHECK --interval=5m --timeout=3s \
CMD curl -f http://localhost/ || exit 1

为了帮助调试失败的探测器,命令在stdout或stderr上写入的任何输出文本(UTF-8编码)将存储在运行状况状态,并可以使用docker inspect查询。这样的输出应该保持短路(只存储当前的4096个字节)。

当容器的运行状况发生更改时,将生成具有新状态的health_status事件。

HEALTHCHECK功能在Docker 1.12中添加。

SHELL

1
SHELL ["executable", "parameters"]

SHELL指令允许用于命令的shell形式的默认shell被覆盖。 Linux上的默认shell是["/bin/sh","-c"],在Windows上是["cmd","/S","/C"]SHELL指令必须以JSON格式写在Dockerfile中。

SHELL指令在Windows上特别有用,其中有两个常用的和完全不同的本机shell:cmdpowershell,以及包括sh的备用Shell。

SHELL指令可以多次出现。每个SHELL指令覆盖所有先前的SHELL指令,并影响所有后续指令。 例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FROM windowsservercore

# Executed as cmd /S /C echo default
RUN echo default

# Executed as cmd /S /C powershell -command Write-Host default
RUN powershell -command Write-Host default

# Executed as powershell -command Write-Host hello
SHELL ["powershell", "-command"]
RUN Write-Host hello

# Executed as cmd /S /C echo hello
SHELL ["cmd", "/S"", "/C"]
RUN echo hello

以下指令可能受SHELL指令的影响,当它们的shell形式用于Dockerfile:RUNCMDENTRYPOINT

以下示例是Windows上的常见模式,可以使用SHELL指令进行简化:

1
2
3
...
RUN powershell -command Execute-MyCmdlet -param1 "c:\foo.txt"
...

docker调用的命令将是:

1
cmd /S /C powershell -command Execute-MyCmdlet -param1 "c:\foo.txt"

这是低效的,有两个原因。首先,有一个不必要的cmd.exe命令处理器(也称为shell)被调用。其次,shell中的每个RUN指令都需要一个额外的powershell -command

为了更有效率,可以采用两种机制之一。 一种是使用JSON形式的RUN命令,如:

1
2
3
...
RUN ["powershell", "-command", "Execute-MyCmdlet", "-param1 \"c:\\foo.txt\""]
...

虽然JSON形式是明确的,并且不使用不必要的cmd.exe,但它需要通过双引号和转义更详细。 备用机制是使用SHELL指令和shell形式,为Windows用户提供更自然的语法,特别是与escape 解析指令结合使用时:

1
2
3
4
5
6
7
# escape=`

FROM windowsservercore
SHELL ["powershell","-command"]
RUN New-Item -ItemType Directory C:\Example
ADD Execute-MyCmdlet.ps1 c:\example\
RUN c:\example\Execute-MyCmdlet -sample 'hello world'

结果是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
PS E:\docker\build\shell> docker build -t shell .
Sending build context to Docker daemon 3.584 kB
Step 1 : FROM windowsservercore
---> 5bc36a335344
Step 2 : SHELL powershell -command
---> Running in 87d7a64c9751
---> 4327358436c1
Removing intermediate container 87d7a64c9751
Step 3 : RUN New-Item -ItemType Directory C:\Example
---> Running in 3e6ba16b8df9


Directory: C:\


Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 6/2/2016 2:59 PM Example


---> 1f1dfdcec085
Removing intermediate container 3e6ba16b8df9
Step 4 : ADD Execute-MyCmdlet.ps1 c:\example\
---> 6770b4c17f29
Removing intermediate container b139e34291dc
Step 5 : RUN c:\example\Execute-MyCmdlet -sample 'hello world'
---> Running in abdcf50dfd1f
Hello from Execute-MyCmdlet.ps1 - passed hello world
---> ba0e25255fda
Removing intermediate container abdcf50dfd1f
Successfully built ba0e25255fda
PS E:\docker\build\shell>

SHELL指令还可以用于修改外壳操作的方式。例如,在Windows上使用SHELL cmd /S /C /V:ON|OFF,可以修改延迟的环境变量扩展语义。

SHELL指令也可以在Linux上使用,如果需要一个替代shell,如zshcshtcsh和其他。

SHELL功能在Docker 1.12中添加