Skip to the content.

Dockerfile参考

Docker可以从一个Dockerfile中读取指令自动创建镜像, Dockerfile是一个包含了用户可以在命令行上所有用于组合镜像的命令。 用户可以使用docker build创建一个连续执行一些命令的自动构建。

本页介绍了能在Dockerfile中使用的命令集。在你阅读完本文之后,你可以参考Dockerfile最佳实践 获得最好的指导。

用法

docker build命令从一个Dockerfile与一个上下文中构建一个镜像。 构建的上下文是一个指定的本地PATHURL的文件集合。PATH为本机上的一个目录,URL为一个git仓库地址。

上下文是一个递归处理的过程,所以,一个PATH包含了其所有的子目录,一个URL包含了该仓库及其子模块。 以下示例展示了使用当前目录作为上下文运行build命令。

$ docker build .
Sending build context to Docker daemon  6.51 MB
...

该构建由Docker守护进程运行,而不是由CLI运行。构建时,最先时最做的就是将整个上下文发送到守护进程。 大多数情况下,最好的方式是使用一个空的目录作为上下文,将Dockerfile放在该目录中。 只添加构建所需的文件到该目录中。

警告: 不要使用根目录/作为上下文,使用/会将你硬盘上的所有内容发送到守护进程

要使用构建上下文中的文件,Dockerfile引用指令指定中指定的文件,比如COPY指令。 为了提升构建性能,可以在上下上下文目录中添加一个.dockerignore文件用于排除文件和目录。 关于如何创建一个.dockerignore文件 的信息在该页面查看。

Dockerfile文件惯例上命名为Dockerfile并放在构建上下文根目录中。你也可以在使用docker build命令时添加上 -f标识指定位于你的文件系统上任意位置的Dockerfile文件。

$ docker build -f /path/to/a/Dockerfile .

你也可以指定一个存储库和标签,在构建成功作为镜像的标识。

$ docker build -t shykes/myapp .

要在构建成功后将镜像票房到多个存储库中,可以添加多个-t标识

$ docker build -t shykes/myapp:1.0.2 -t shykes/myapp:laest .

Docker守护进程在运行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会尽可能的使用中间镜像(缓存)以显著加速构建过程。这由控制台输出的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 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

格式

Dockerfile 格式如下:

# 注释
指令 参数

指令对大小写不敏感,然后将指令转换成大写可以更容易区分指令与参数。 Docker运行Dockerfile中的指令是按照顺序进行。一个Dockerfile必须是以FROM指令开始。 FROM指令指明了你创建的基础镜像。FORM指令只能在一个或多个ARG指令之前, ARG指令声明在Dockerfile中FROM行使用的参数。

如果一行以#开始,Docker认为其是一个注释,除非该行是一个合法解析指令。 一行其他地方的#标志会当作参数对待。可以使用如下语句:

# Comment
RUN echo 'we are running some # of cool things'

续行标识不能用于注释中。

解析指令

TODO

环境变更

环境变量(用ENV语句声明)也可以在某些指令中使用,作为Dockerfile解释的变量。 将类似变量的语句包含在字符串在还可以用来处理转义。

环境变量在Dockerfile中使用$variable_name${variable_name}标记。他们被等效对待, 并且大括号语法用于解决不带空格的亦是名称的问题,比如${foo}_bar${variable_name}语法还支持以下特定的标准bash修饰符:

word可以是任何字符串,也可以是其他环境变量。

可以在一个变量前加\来转义:\$foo\$bar,以下示例将$foo${foo}分别转为字符。 #表示解析后的结果。

FROM busybox
ENV foo /bar
WORKDIR ${foo}   # WORKDIR /bar
ADD . $foo       # ADD . /bar
COPY \$foo /quux # COPY $foo /quux

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

以及

1.4之前,ONBUILD指令不支持环境变量,即使与上述其他指令组合

环境变量将会在整个指令中为每个变量使用相同的值,换句话说,在以下的例子中, def=hello,而不是bye。然而ghi=bye,因为他与abc=bye不在同一条指令中。

.dockerignore file

在docker CLI发送context到docker守护进程之前,docker CLI将会寻找context根目录下一个名为.dockerignore的文件。 如果该文件存在,docker CLI将会修改context,使其排除该文件中匹配的文件目录。 这有助于避免将不必要的大的、敏感的文件或目录发送到守护进程,也能避免用户通过ADDCOPY命令将这些文件或目录添加到镜像。

CLI将.dockerignore文件解析为以换行符作为分割符的匹配模式列表,context的根目录将会作为.dockerignore中所有匹配的根目录。 比如,模式/foo/barfoo/bar都会排除PATH或本地git仓库根目录下foo子目录中名为bar的文件或目录。

如果.dockerignore中的一行以#开始,那么该行被认为是注释,在CLI解析时将会被忽略。

下面是一个.dockerignore列子

# comment
*/temp*
*/*/temp*
temp?

| Rule | Behavior | | ————– | ——————————————————————————————————- | | # comment | 注释、忽略 | | /temp | 排除根目录中的直接子目录中任何以temp开头的文件或目录,比如文件/somedir/temporary.txt、目录/somedir/temp | | //temp* | 排除根目录中的二级子目录内的以temp开头的文件或目录,比如 /somedir/subdir/temporary.txt | | temp? | 排除根目录中名称为temp后跟一个字符的文件或目录,比如/tempa/tempb |

匹配使用Go的filepath.Match规则,预处理步骤中将会删除开头与结尾的空白符, 使用filepath.Clean清除...元素元素。预处理后的空白行将会被忽略。

除了Go的filepath.Match规则之外,Docker还支持一个特殊的通配符**,用来匹配任意数量的目录,包括没有目录(including zero)。 比如**/*.go将会排除context根目录下所以有.go结尾的文件。

一行以!(感叹号)开头可以用来标识例外的情况,下面是一个使用该机制的例子:

*.md
!README.md

排除除了README.md之外的所有md文件。

!符号位置的影响行为:.dockerignore最后一行匹配决定一文件或目录是包含还是排除。考虑如下情况:

*.md
!README*.md
README-secret.md

不包含任何markdown文件,除了README之外,而且要排除README-secret.md

现在考虑如下情况:

*.md
README-secret.md
!README*.md

所有的README文件将会被包含进来,中间这行没有起效,因为!README*.md也匹配了README-secred.md,并且还是在最后匹配的。

你甚至可以使用.dockerignore排除Dockerfile和.dockerignore文件。 但是这些文件依然会被发送到守护进程,因为需要它们来完成任务,但ADDCOPY指令将不会复制它们到镜像。

最后,你可能希望能够指定哪些文件可以包含进来,而不是排除哪些文件。 为了达到这个目的,你可以指定*作为第一个匹配模式,然后再使用一个或多个!感叹号模式。

注: 历史原因,.匹配模式已被忽略。

FROM

FROM <image> [AS <name>]

FROM <image>[:<tag>] [AS <name>]

FROM <image>[@<digest>] [AS <name>]

FROM指令初始化一个新的构建阶段并且为接下来的指令设置一个基础镜像。 一个合法的Dockerfile必须以一个FROM指令开始。image参数可以是任意合法的镜像, 可以非常容易的从公开仓库中得到。

FROM可以在单个Docker文件中多次出现以创建多个图像,或者使用一个构建阶段作为另一个的依赖。 在每个新的FROM指令之前,简单地记录通过提交输出的最后一个图像ID。每个FROM指令清除由先前指令创建的任何状态。

ARG与FROM的相互影响

FROM指令支持在第一个FROM指令前使用ARG指令声明的变量。

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

RUN

RUN有两种形式:

RUN指令将在当前镜像最上层创建一个新的层来执行并提交执行结果,该接果将会用于Dockerfile后续的步骤。

分层执行RUN指令并提交结果,符合Docker的核概念,提交变更成本很低,容器可以从镜像的任意层创建。

执行形式可以避免shell脚本字符被修改,还可以在一个没有指定shell运行环境的基础镜像中执行命令。

shell形式的默认shell可以通过SHELL命令修改。 在shell形式下可以使用一个(反斜线)来连接位位于多行的一条指令。如:

RUN /bin/bash -c 'source $HOME/.bashrc; \
echo $HOME'

等同于

RUN /bin/bash -c 'source $HOME/.bashrc; echo $HOME'

注: 如果不使用/bin/sh而想使用其他的shell来运行命令,可以使用执行形式指定一个shell。 比如:`RUN [“/bin/bash”, “-c”, “echo hello”]

注: 执行形式将会被转成JSON数据,这意味着你必须使用双引号(”)来包裹一个词,而不是单引号(’)。

注: 与shell形式不同,执行形式不会调用shell命令,不会执行正常的shell处理程序。 比如执行RUN ["echo", "$HOME"],$HOME并不会发生变量替换。 如果你希望shell处理程序执行,可使用shell形式或直接执行一个shell,如RUN [ "sh", "-c", "echo $HOME" ]。 当你使用执行形式或直接执行shell,与shell形式一样,是由正在执行环境变量扩展的shell在处理,而不是docker。

注: 在JSON形式下,必需转义反斜线,尤其是在将反斜线作为路径分隔符的windows上。 如:["c:\windows\system32\tasklist.exe"]将会被视为shell形式,因为它不是合法的JSON。 正确的语法应为:`[“c:\windows\system32\tasklist.exe”]

RUN指令的缓存不会自动清理,将会用于下一次构建。 如运行RUN apt-get dist-upgrade -y的缓存在一次的构建时将会被使用。 但可以通过--no-cache标识使RUN指令的缓存失效,如docker build --no-cache

更多信息见Dockerfile 最佳实践指南

RUN指令的缓存可以通途ADD指令使其无效,详见

已知问题(RUN)

Issue 783 是关于文件权限的问题,可能在AUFS文件系统上出现。在你尝试删除一个文件的时候可能会收到该提示。

如果系统为最近的aufs版本(可以设置dirperm挂载配置),Docker将会尝试通过使用dirperm选择挂载该层来自动修复这个问题。 dirperm1选择的详细信息可以在aufs主页找到。

如果你的系统不支持dirperm1,该issue提供了一个解决方案。

CMD

CMD指令有三种使用形式

一个Dockerfile中只能出现一次CMD指令,如果出现多次,只有最后一次出现的起作用。

CMD一个主要目的是为一个执行容器提供默认值,该值可以包含可执程序,也可以省略,如果省略了,你需要指一个ENTRYPOINT指令。

注: 如果CMD用来为ENTRYPOINT指令提供默认参数,CMDENTRYPOINT指令都要符合JSON数组格式。

注: 执行形式将会解析为JSON格式数组,所以你必须使双引号来包裹语句,而非单引号。

注: 与shell形式不同,执行形式不会调用shell程序,不会发生正常的shell程序处理。 如CMD ["echo", "$HOME"]不会在$HOME上发生变量替换。如果你希望shell处理程序生效,你可以使用shell形式或直接执行shell程序。 如CMD ["sh", "-c", "echo $HOME"],当你使用执行形式或直接执行shell,与shell形式一样,是由正在执行环境变量扩展的shell在处理,而不是docker。

shell格式与可执行形式的CMD指令设置的命令将会在镜像运行是执行。

如果你使用了shell形式的CMD指令,其<command>部份将会运行在/bin/sh -c中:

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

如果你不想在shell中运行你的<command>,你必须使用JSON数组格式定义命令,并且为可执行程序给定完整的路径。 数组形式是CMD首选形式,所有的附加参数必须独立的以字符串形式置于数组中。

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

如果你希望容器每次执行同样的程序,那么你应该考虑ENTRYPOINTCMD结合使用,详见ENTRYPOINT

如果用户在运行docker run命令时指定了参数,将会覆盖默认的CMD指令。

注: 不要混淆RUNCMDRUN实际上是运行命令并提交结果,CMD在构建是不会执行任何指令, 只是为镜像提供预备(intended)(译注:相当于默认的意思吧,就是如果在docker run中没有提供参数,就使用CMD提供的了)的命令。

LABEL

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

LABEL指令添加元数据到一个镜像中(metadata),一个LABEL为一个key-value对,其值与LABEL用空格隔开。 可以像命令中一样使用引号和反斜杆。下面是一些使用例子:

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,Docker推荐尽可能使用单个LABEL指令组合所有的值。 每个LABEL会产生一个新的层,从而导致镜像效率低下。下面的例子的结果将会作为镜像的单个层:

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

上面的例子也可以写成:

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

标签附加在FROM提供的镜像里,如果Docker遇到标签的键与已存在的键相冲突的情况,新的值将会覆盖原来键所属的值。 使用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"
},

MAINTAINER (已废弃)

EXPOSE

EXPOSE <port> [<port>/<protocol>...]

EXPOSE指令告诉Docker容器运行时监听的网络端口。你可以指定TCP或UDT的端口,如果没有指定通信协议,默认为TCP端口。

EXPOSE指令并非真正的开放端口。它的功能是为在构建镜像和使用镜像人的之间创建一个关于哪个端口作为预设的开放端口的描述。 真正开放端口是在运行容器的时候,在docker run时使用-p标记指定开放与映射一个或多个端口, 或者使用-P开放所有已设置的端口并将其映射到高级别(high-order)端口。

在主机系统上设置端口重定向,参考使用-P标记docker network命令支持创建一个网络,容器可以不暴露或发布指定端口而使用该网络通信,因为容器联结到该网络可以与任意其他端口通信。 详细信息该特性概览

ENV

DockerfileENT指令介绍

为了使用新的软件更容易运行,可以使用ENV为容器中的软件更新PATH环境变量, 比如ENV PATH /usr/local/nginx/bin:$PATH可以确保CMD ["nginx"]正确运行。

ENV指令在为服务指供必要的环境变量时也非常有用,比如Postgres的PGDATA

ENV也可以用来设置公共的版本号,使得维护版本号变得非常容易:

ENV PG_MAJOR 9.3
ENV PG_VERSION 9.3.4
RUN curl -SL http://example.com/postgres-$PG_VERSION.tar.xz | tar -xJC /usr/src/postgress &&ENV PATH /usr/local/postgres-$PG_MAJOR/bin:$PATH

类似于程序中的常量(不是硬编码),该方法可以让你通过变更一个ENV指令而达到自动变容器中的软件的目的。

ADD or COPY

ADD指令介绍 COPY指令介绍

虽然ADDCOPY功能类似,但通常而言,COPY是首选,因为它比ADD更透明(transparent)。 COPY只支持本地文件到容器的基本复制功能,而ADD有一些隐含的特性(如本地tar文件提取、远程URL支持)。 所以ADD的最佳使用方法是将本地文件自动提取到镜像,如:ADD rootfs.tar.xz /.

如果Dockerfile中有多个步骤需要使用不同的文件,对每个文件使用COPY指令比次复制所有的方法要好。 如果特别的指明了需要依赖文件变化,这将确保每个步骤的构建缓存失效(is only invalidated. 译注:是指即时清除缓存吗?)

COPY requirements.txt /tmp/
RUN pip install --requirement /tmp/requirements.txt
COPY . /tmp/

如果你将COPY ./tmp/放在RUN之前,会导致RUN步骤的缓存失效数减少。 (Results in fewer cache invalidations for the RUN step, than if you put the COPY . /tmp/ before it. 译注:说不太明白)

基于图形大小的考量,强烈建议不要使用ADD从远程URL上拉取安装包,而应使用curlwget代替。 这样你可以删除在解压后不再需要的文件,而不必在镜像中再创建一层。比如,你应该避免如下的方式:

ADD http://example.com/big.tar.xz /usr/src/things/
RUN tar -xJf /usr/src/things/big.tar.xz -C /usr/src/things
RUN make -C /usr/src/things all

而是如此:

RUN mkdir -p /usr/src/things \
    && curl -SL http://example.com/big.tar.xz \
    | tar -xJC /usr/src/things \
    && make -C /usr/src/things all

对于其他不需要的自动提取功能的项(文件、目录),你应该始终使用COPY

ENTRYPOINT

ENTRYPOINT 有两种形式:

ENTRYPOIN允许你配置容器以何种方式运行,下面例子将使用使用nginx的默认内容启动,并监听80端口:

docker run -i -t --rm -p 80:80 nginx

docker run <image>命令行参数将会全部追加到执行形式的ENTRYPOINT后,并会覆写CMD指令指定的所有元素。 这允许参数传递到入口点(entry point),如docker run <image> -d将会把-d参数传递到入口点。 你可以通过docker run --entrypoint标记覆写ENTRYPOINT指令。

shell形式阻止任何CMDrun命令行参数,这种方式有一个缺点:ENTRYPOINT将会作为/bin/sh -c的子命令运行,因而不会传入信号。 这意味着执行程序不会成为容器的PID 1,不能收到Unix信号,因而你的执行程序不会收到docker stop <container>的终止信号SIGTERM

在Dockerfile中,只有最后一个ENTRYPOINT指令才会生效。

执行形式 ENTRYPOINT 示例

你可以使用ENTRYPOINT的执行形式设置稳定的默认命令与参数,然后使用CMD两种形式之一设置可变参数的默认值。

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

运行容器时将会看到top是唯一的进程:

$ docker run -it --rm --name test  top -H
top - 08:25:00 up  7:27,  0 users,  load average: 0.00, 0.01, 0.05
Threads:   1 total,   1 running,   0 sleeping,   0 stopped,   0 zombie
%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
KiB Mem:   2056668 total,  1616832 used,   439836 free,    99352 buffers
KiB Swap:  1441840 total,        0 used,  1441840 free.  1324440 cached Mem

  PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND
    1 root      20   0   19744   2336   2080 R  0.0  0.1   0:00.04 top

可以使用docker exec查看更多结果:

$ docker exec -it test ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  2.6  0.1  19752  2352 ?        Ss+  08:24   0:00 top -b -H
root         7  0.0  0.1  15572  2164 ?        R+   08:25   0:00 ps aux

你可以使用docker stop test优雅的停止top进程。

下面Dockerfile展示了使用ENTRYPOINT使用FOREGROUND运行Apache。

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"]

如果你需要为某个可执行程序书写一个启动器,你需要确保最终执行程序能接收从execgosu命令发出Unix信号:

#!/usr/bin/env 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 "$@"

如果你需要在停止的时候做一些特别的清理(或与其他容器通信),或是协调多个执行程序,你可能需要确保ENTRYPOINT脚本能接收Unix信号, 通过他们做一些事情:

#!/bin/sh
# 注:该脚本使用sh书写,所以也可以运行在busybox容器中

# 如果服务停止后你还需要手动做一些清理工作,请使用trap命令。
# 或者需要在一个容器中启动多个服务
trap "echo TRAPed signal" HUP INT QUIT TERM

# 启动一个服务在后台运行
/usr/sbin/apachectl start

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

# 停止服务并清理
echo "stopping apache"
/usr/sbin/apachectl stop

echo "exited $0"

如果使用docker run -it --rm -p 80:80 --name test apache运行该镜像,可以使用docker exec查看容器进程, 或使用docker stop后要求脚本停止Apache:

$ docker exec -it test ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.1  0.0   4448   692 ?        Ss+  00:42   0:00 /bin/sh /run.sh 123 cmd cmd2
root        19  0.0  0.2  71304  4440 ?        Ss   00:42   0:00 /usr/sbin/apache2 -k start
www-data    20  0.2  0.2 360468  6004 ?        Sl   00:42   0:00 /usr/sbin/apache2 -k start
www-data    21  0.2  0.2 360468  6000 ?        Sl   00:42   0:00 /usr/sbin/apache2 -k start
root        81  0.0  0.1  15572  2140 ?        R+   00:44   0:00 ps aux
$ docker top test
PID                 USER                COMMAND
10035               root                {run.sh} /bin/sh /run.sh 123 cmd cmd2
10054               root                /usr/sbin/apache2 -k start
10055               33                  /usr/sbin/apache2 -k start
10056               33                  /usr/sbin/apache2 -k start
$ /usr/bin/time docker stop test
test
real	0m 0.27s
user	0m 0.03s
sys	0m 0.03s

注:可以使用--entrypoint覆写ENTRYPOINT配置,但是只能设置执行的二态(binary,译注:不理解)。

注:执行形式将会解析为JSON数组,所以命令需要包在双引号内,单引号不行。

注:与shell形式不同,执行形式不会调用shell程序,不会发生正常的shell程序处理。 如ENTRYPOINT [ "echo", "$HOME" ]不会在$HOME上发生变量替换。如果你希望shell处理程序生效,你可以使用shell形式或直接执行shell程序。 如ENTRYPOINT [ "sh", "-c", "echo $HOME" ],当你使用执行形式或直接执行shell,与shell形式一样,是由正在执行环境变量扩展的shell在处理,而不是docker。

shell形式ENTRYPOINT例子

你可以为ENTRYPOINT指定一个纯字符串作为其值,这将会运行在/bin/sh -c中。shell形式使用shell程序替换shell环境变量, 并且会忽略所有CMDdocker run命令行参数。为了确保docker stop能正确的发送信号给长时间运行的程序,你需要使用exec启动程序:

FROM ubuntu
ENTRYPOINT exec top -b

运行后你可以看到一个PID1进程:

$ 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时会干净利落的退出:

$ /usr/bin/time docker stop test
test
real	0m 0.20s
user	0m 0.02s
sys	0m 0.04s

如果你忘记了在ENTRYPOINT开始处使用exec

FROM ubuntu
ENTRYPOINT top -b
CMD --ignored-param1

运行后(为了下面步骤,指定一个name):

$ 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

从输出内容可以看到并不是PID 1

当运行docker stop test时,该容器并没有直接退出,stop命令会在超时后强制发送SIGKILL

$ 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

理解CMD与ENTRYPOINT相互影响

CMDENTRYPOINT指令都是定义一个容器运行时执行的命令,下面有一些规则描述他们之间的协作。

  1. Dockerfile应该在结尾定义一个CMDENTRYPOINT
  2. 当容器作为一个可执行程序时,应该定义ENTRYPOINT
  3. CMD应该用来作为定义ENTRYPOINT的默认参数,或者在容器中执行点对点(ad-hoc)的命令
  4. 在容器启动时,CMD将会被其他替代参数覆写

下面表格展示了ENTRYPOINTCMD组合时,什么命令会被执行:

  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

VOLUME ["/data"]

VOLUME指令创建一个具有指定全称的挂载点,将其标记为从本地主机或其他容器联结到容器挂载卷(译注:这句有点不通顺)。 其值可以是JSON数组:VOLUME ["/var/log/"],或者包含多个参数的纯字符串,如:VOLUME /var/logVOLUME /var/log /var/db。 Docker客户端的更多信息、示例及挂载相关指令,参考使用Volumes共享目录文档。

Docker run命令使用镜像中指定位置的所有数据初始化新创建的卷,考虑如下Dockerfile片段:

FROM ubuntu
RUN mkdir /myvol
RUN echo "hello world" > /myvol/greeting
VOLUME /myvol

运行docker run,该镜像会在/myvol上创建一个新的挂载点,并且复制greeting文件到新创建的卷上。

指定卷应该注意:

USER

USER <user>[:<group>] or
USER <UID>[:<GID>]

USER指令设置用户名(或UID)和可选的运行镜像时、以及Dockerfile中随后的RUN,CMDENTRYPOINT指令使用的用户组(或GID)。

警告: 当用户没有所属组时(does doesn’t have a primary group),镜像将会使用root组运行。

WORKDIR

WORKDIR /path/to/workdir

WORKDIR为Dockerfile中随后的RUN,CMD,ENTRYPOINT,COPYADD指令提供工作目录。 如果WORKDIR不存在,即使在随后的指令中并没有使用它,工作目录也会被创建。

WORKDIR指令可以在Dockerfile中出现多次,如果设置了一个相对路径,它会相对于上一个WORKDIR指令,如:

WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd

该Dockerfile中最终的pwd的结果为/a/b/c

WORKDIR可以解析在此之前使用ENV设置的环境变量,你只能使用在Dockerfile中显示设置的环境变量,如:

ENV DIRPATH /path
WORKDIR $DIRPATH/$DIRNAME
RUN pwd

该Dockerfile中最终pwd结果为path/$DIRNAME

ARG

ARG <name>[=<default value>]

ARG指令定义了一个变量,用户可以在使用docker build命令构建时添加--build-arg <varname>=<value>标志传递给构建器(builder)。 如果用户指定了在Dockerfile中未定义的构建参数,则构建会输出警告。

[Warning] One or more build-args [foo] were not consumed.

一个Dockerfile中可以包含一个或多个ARG,以下的Dockerfile是合法的:

FROM busybox
ARG user1
ARG buildno
...

警告: 不建议使用构建时变量来传递安全相关信息,比如github keys,用户凭证等。 因为任何用户都可以使用docker history命令看到构建时的变量值。

默认值

ARG指令可以设置一个默认值:

FROM busybox
ARG user1=someuser
ARG buildno=1
...

如果一个ARG指令有默认值,在构建时如果没有传入它的值时,构建器将会使用默认值。

作用域

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

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

一个用户使用如下方式构建:

$ docker build --build-arg user=what_user .

第2行计算结果为some_user,因为user是在第3行定义的(译注:此时ARG参数还没生效呢), 第4行USER计算结果为what_user,因为此时user已经定义(译注:在第3行),并且从命令行传入了what_user值。 在ARG指令定义变量之前,任何使用变量的行为都会得到空字符串。

ARG指令在其定义的构建阶段结束时失效(out of scope),要在多个阶段使用参数,需要在每个阶段包含ARG指令。

FROM busybox
ARG SETTINGS
RUN ./run/setup $SETTINGS

FROM busybox
ARG SETTINGS
RUN ./run/other $SETTINGS

使用ARG变量

你可以使用ARGENV指令来指定可用于RUN指令的变量,使用ENV指令定义的环境变量总是覆盖相同名称的ARG指令定义的变量。 考虑下面这个包含一个ARGENV指令的Dockerfile:

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

假设使用如下命令构建该镜像:

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

该情况下,RUN指令使用v1.0.0替代用户传入的v2.0.1。该行为类似于shell脚本中本地作用域变量将覆盖作为参数传入的变量或他处继承的变量。

在上面的例子使用不同的ENV指定值,你可以在ARGENV指令之间创建更有用的交互:

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

不同于ARGENV的始终存在于构建镜像中,思考不使用--build-arg标识:

$ docker build .

该例中CONT_IMG_VER仍然存在镜像中,但是其值为-v1.0.0,因为取的是第3行设置的默认值。

此示例中的变量扩展技术允许你从命令行传递参数,并通过使用ENV指令将它们一直保存在最终镜像中,变量扩展只支持部份Dockerfile指令。

预定义ARG

Docker有一组预定义ARG变量,你可以直接使用而不必使用相应的ARG指令声明:

可以在命令行中使用标识传入它们的值:

--build-arg <varname>=<value>

默认情况下,这些预定义变量排除在docker history输出之外,排队它们降低了在HTTP_PROXY变量中意外泄露敏感认证信息的风险。

考虑使用--build-arg HTTP_PROXY=http://user:pass@proxy.lon.example.com构建如下Dockerfile:

FROM ubuntu
RUN echo "Hello World"

在这种情况下,HTTP_PROXY变量的值在docker history中不可用,也不会缓存。如果你想修改地址, 并且你的代理服务器改为http://user:pass@proxy.sfo.example.com,不会导致后续的构建发生缓存遗漏(cache miss)。

如果你想覆盖此行为,你可以通过添加ARG声明:

FROM ubuntu
ARG HTTP_PROXY
RUN echo "Hello World"

构建该Dockerfile时,HTTP_PROXY将保留在docker history中,并且更该值会使构建缓存失效。

对构建缓存的影响

因为ENV的存在,ARG变量不会保留在构建镜像中。然而ARG变量会以类似方式影响构建缓存。 如果Dockerfile定义了一个ARG变量,其值与此前构建时的值不同,那么在第一次使用时将会出现缓存遗漏(cache miss)。 具体而言,ARG指令之后的所有RUN指令隐含的(作为环境变量)使用ARG变量,因而会可以引发缓存遗漏。 所有的预定义指令都不会缓存,除非在Dockerfile中有其声明。

考虑如下两个Dockerfile

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

该例中,缓存遗漏发生在第3行。因为EVN中的变量的值引用了ARG变量,并且使用命令行使该值发生了变化,所以发生了遗漏。 此例中ENV命令便镜像包含该值。

如果ENV指令覆写了同类ARG指令,如:

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

ONBUILD指令向镜像添加一个触发器(trigger)指令,当该镜像作为其他构建的基础镜像时, 该触发器会在下游构建的context中执行,就像它直接插入到下游Dockerfile中FROM指令后面一样。

任何指令都可以注册一个触发器。

如果你的构建镜像将用作其他镜像的基础构建,这会非常有用。 比如可以通过用户指定的配置自定义的应用程序的构建环境或守护进程。

比如:如果你的镜像是一个可复用的Python应用程序构建器,那么需要将应用的源码添加到指定的目录。 并且在之后可能需要调用构建脚本。此时你无法调用ADDRUN指令,因为此时你还不知道应用程序源码, 并且每个应用构建都会不同。你可以简单的为开发人员提供一个Dockerfile样板,复制到各自的应用程序中。 但由于它与特定的应用代码混合,因此效率低下,容易出错并且难以更新维护。

解决方案是使用ONBUILD注册高级指令到运行后、下一个构建阶段之前。

工作原理:

  1. 当遇到ONBUILD指令时,构建器添加一个触发器到正在构建镜像的元数据上,该指令不会影响当前构建。
  2. 构建结束时,所有的触发器列表存储在镜像的清单的OnBuild键下,可以使用docker inspect命令查看。
  3. 稍后可能使用FROM指令将该镜像作为一个新构建的基础,作为FROM指令处理的一部,下游构建器将会查找ONBUILD的触发器, 并按其注册顺序执行。如果某一触发器执行失败,FROM指令将会退出从而导致构建失败, 如果所有的触发器都成功执行,FROM指令完成并继续构建过程。
  4. 触发器会在最终镜像执行完成后清除。(译注:后面还有一句不知如何翻译: In other words they are not inherited by “grand-children” builds.)

例如,你可能添加如下内容:

[...]
ONBUILD ADD . /app/src
ONBUILD RUN /usr/local/bin/python-build --dir /app/src
[...]

警告: 不能使用ONBUILD ONBUILD链接ONBUILD指令

警告: ONBUILD指令可能不触发FROMMAINTAINER指令

STOPSIGNAL

STOPSIGNAL signal

STOPSIGNAL指令设置发送到容器使其退出的系统调用号,该信号可以是一个与系统调用表相匹配的无符号数字,比如9, 或者是一个SIGNAME格式的信号名称,如SIGKILL

HEALTHCHECK

HEALTHCHECK有两种形式:

HEALTHCHECK指令告诉容器如何检测一个容器能正常工作,比如可以检测一个web服务是否卡在一个无限循环中, 从而不能处理新的连接请求,即使该服务处理进程还在运行,也可以进行检测。

当一个容器指定了了healthcheck时,它除了正常状态外,还有一个健康状态,该状态最初是staring(译注:原文:This status is initially staring)。 每当一个健康检测通过时,无论之前的状态是什么,该状态都会变为healthy,经过一定次数的连续失败后,该状态变为unhealthy

CMD之前的OPTIONS的值可以为:

健康检查在容器启动后interval秒开始首次运行,每次检测完之后,隔interval秒后再次检测。

如果单次健康检测耗时超过timeout,则认为检测失败。

如果容器健康检测连续retries次失败,则认为容器为unhealthy

启动时间为容器初始化的时间,在此期间检测到的失败不计入最大重试次数。但是,如果健康检测在开始其间成功, 则认为容器已启动,并且所有连续的失败将计入最大重试数中。

一个Dockerfile中只能有一个HEALTHCHECK指令,如果你列出了多个,只有最后一个生效。

CMD之后的命令可以是shell命令(如:HEALTHCHECK CMD /bin/check-running)或执行形式数组(同Dockerfile其他命令一样,比如ENTRYPOINT)。

命令的退退出状态标明容器的运行状态,可能的值为:

比如,每隔五分钟左右检测网站服务能够在3秒内提供其主页面:

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

为了debug失败原因,命令在stdout或stderr上输出的任何内容(utf-8编码)都会存在健康状态里, 并且可以通过docker inspect查询。此类输出应该保持短小(仅存储输出的前4096个字节)。

当容器的健康状态发生变化时,将会使用新的状态生成一个health_status事件。

HEALTHCECK在Docker 1.12版本中加入。

SHELL

SHELL ["executable", "parameters"]

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

SHELL指令在Windows上特别有用,Windows中有两个常用的但差别非常大的原生shell:cmdpowershell, 以及包含sh的备用shell。

SHELL指令可以出现多闪,每个SHELL指令覆写之前所有的SHELL指令,并且影响其后的所有指令。如:

FROM microsoft/windowsservercore

# 执行 cmd /S /C echo default
RUN echo default

# 执行 cmd /S /C powershell -command Write-Host default
RUN powershell -command Write-Host default

# 执行 powershell -command Write-Host hello
SHELL ["powershell", "-command"]
RUN Write-Host hello

# 执行 cmd /S /C echo hello
SHELL ["cmd", "/S"", "/C"]
RUN echo hello

RUN, CMDENTRYPOINT指令的shell形式会受SHELL指令影响。

下面的示例是在Windows查找可以使用SHELL流化(streamlined)的常见模式:

原文: The following example is a common pattern found on Windows which can be streamlined by using the SHELL instruction:

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

docker调用该命令结果为:

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

导致无效有两个原因,其一,有个不必要的cmd.exe命令处理器(又称shell)被调用。 其二,每个shell形式的RUN指令需要在命令前添加一个额外的powershell -command前缀。

要使期有效,有两种机制可用,其一是使用RUN命令的JSON形式:

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

虽然使用JSON形式可以提供明确的指令,并且不会使用不必要的cmd.exe,但它需要使用双引号与转义,因而变得冗长。 替代机制是使用SHELL指令和shell形式,为Windows用户提供更接近原有的语法,在与escape解析指令结合使用是更为明显:

# escape=`

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

结果为:

PS E:\docker\build\shell> docker build -t shell .
Sending build context to Docker daemon 4.096 kB
Step 1/5 : FROM microsoft/nanoserver
 ---> 22738ff49c6d
Step 2/5 : SHELL powershell -command
 ---> Running in 6fcdb6855ae2
 ---> 6331462d4300
Removing intermediate container 6fcdb6855ae2
Step 3/5 : RUN New-Item -ItemType Directory C:\Example
 ---> Running in d0eef8386e97


    Directory: C:\


Mode                LastWriteTime         Length Name
----                -------------         ------ ----
d-----       10/28/2016  11:26 AM                Example


 ---> 3f2fbf1395d9
Removing intermediate container d0eef8386e97
Step 4/5 : ADD Execute-MyCmdlet.ps1 c:\example\
 ---> a955b2621c31
Removing intermediate container b825593d39fc
Step 5/5 : RUN c:\example\Execute-MyCmdlet 'hello world'
 ---> Running in be6d8e63fe75
hello world
 ---> 8e559e9bf424
Removing intermediate container be6d8e63fe75
Successfully built 8e559e9bf424
PS E:\docker\build\shell>

SHELL指令也可以用于修改shell操作方式,例如,在Windows上使用HELL cmd /S /C /V:ON|OFF, 可以修改延时的环境变量扩展语义(原文:delayed environment variable expansion semantics could be modified. 译者不明白这个命令做什么的)。

SHELL指也可以在Linux上使用替换shell,如zshcsh,tcsh等。

SHELL在Docker 1.12版本中引入。

Dockerfile 示例

下面你可以看到一些Dockerfile语法的例子,如果你对更真实的内容感兴趣,请查看Docker化示例

# Nginx
#
# VERSION               0.0.1

FROM      ubuntu
LABEL Description="This image is used to start the foobar executable" Vendor="ACME Products" Version="1.0"
RUN apt-get update && apt-get install -y inotify-tools nginx apache2 openssh-server
# Firefox over VNC
#
# VERSION               0.3

FROM ubuntu

# Install vnc, xvfb in order to create a 'fake' display and firefox
RUN apt-get update && apt-get install -y x11vnc xvfb firefox
RUN mkdir ~/.vnc
# Setup a password
RUN x11vnc -storepasswd 1234 ~/.vnc/passwd
# Autostart firefox (might not be the best way, but it does the trick)
RUN bash -c 'echo "firefox" >> /.bashrc'

EXPOSE 5900
CMD    ["x11vnc", "-forever", "-usepw", "-create"]
# Multiple images example
#
# VERSION               0.1

FROM ubuntu
RUN echo foo > bar
# Will output something like ===> 907ad6c2736f

FROM ubuntu
RUN echo moo > oink
# Will output something like ===> 695d7793cbe4

# You'll now have two images, 907ad6c2736f with /bar, and 695d7793cbe4 with
# /oink.