Dockerfile 参考手册(二):指令介绍

博客原文

本文接着上一篇文章《Dockerfile 参考手册(一)》接续Dockerfile相关的学习。本文主要介绍Dockerfile中最重要的部分,构建镜像的主角:指令。
文档是基于Docker v17.09 版本。
文章内容完全是翻译官方文档

01 Dockerfile中常用的指令

这里总结Dockerfile中常用指令的用法。

01.1 FROM

FROM <image> [AS <name>]

或者

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

或者

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

FROM指令初始化一个新的编译阶段,为后续的指令设置一个基础镜像,由于这个原因,一个有效的Dockerfile文件必须以FROM指令开头,这个镜像可以是任何有效的镜像,从公共仓库拉取一个镜像作为开始比较简单。

  • Dockerfile文件中,ARG指令是唯一一个可以放在FROM之前的指令。参见Understand how ARG and FROM interact
  • 在一个Dockerfile文件中,FROM指令可以出现多次,用于创建多个镜像或者使用一个作为另一个编译阶段的依赖。不过,每一个新的FROM指令之前都会输出一个提交之后的最新的镜像ID。每一个FROM指令都会清楚之前指令创建的任何状态。
  • tag 或者 digest的值是可选的,如果你省略它们,编译器都会认为是默认的标签latest,如果编译器没有发现这个tag的值,就会返回一个错误。
理解ARG和FROM是如何相互作用的

在第一个FROM指令之前,ARG指令声明的任何变量,FROM指令都是支持的。

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

01.2 RUN

RUN指令有两种形式:

  • RUN <command>(shell的形式,命令运行在shell中,在Linux上默认的是/bin/sh -c,在windows上默认的是cmd /S /C
  • RUN ["executable", "param1", "param2"](exec的形式)

RUN指令是在当前镜像的最上面的一个新层上执行任何命令的,然后提交结果,提交结果之后的镜像被用于Dockerfile中下面步骤的操作基础。

RUN指令的分层性和提交即生成镜像符合Docker的核心理念,其中提交是廉价的,在一个镜像历史的任何一个点都是可以创建容器的,这很像源代码的管理。

exec的形式可以避免shell字符串写死,可以传参数,RUN命令还可以指定执行基础镜像中不包含的可执行shell。

默认的shell形式可能被SHELL指令修改。

在shell形式中,你可以使用反斜杠\,在下一行继续编写同一条RUN指令,例如下面两行:

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

它们和下面这一行是同样的作业:

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

注意:为了使用不同shell,而不使用'/bin/sh',使用exec形式转化你渴望使用的shell,例如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环境下,反斜杠是路径分隔符。下面一行将会被认为是shell的形式处理,而不是一个有效的JSON,然后就意想不到的失败了: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最佳实践指导

RUN指令的缓存也可能被ADD指令置为失效,详细参照下面的介绍

已知问题[RUN]
  • Issue 783,当使用AUFS文件系统时可能会遇到文件权限问题。例如,在你尝试rm一个文件的时候会注意到这个问题。

  • 对于一个有最新aufs版本(可能会设置dirperm1的挂载操作)的系统来说,docker可能会尝试自动修复这个问题,使用的方法是,挂载一个有dirperm1操作的层。关于dirperm1操作的更多详细信息可以在aufs man page上找到。

如果你的系统不支持dirperm1,这个问题描述了一个变通的方法。

01.3 CMD

CMD指令有三种形式:

  • CMD ["executable","param1","param2"](exec形式,一个首选的形式)
  • CMD ["param1","param2"](作为ENTRYPOINT的默认参数)
  • CMD command param1 param2(shell形式)

在一个Dockerfile文件中只能有一个CMD指令,如果你使用多个CMD,只有最后一个是生效的。

CMD命令的主要目的是为运行时的容器提供一些默认参数和命令,这些默认包括一些可执行的文件,或者可以省略可执行文件,这种情况下你必须同时指定一个ENTRYPOINT指令。

注意:如果CMD指令用于为ENTRYPOINT指令指定默认参数,CMDENTRYPOINT指令必须都是JSON数组的格式。

注意:exec形式是被作为JSON数组解析的,这意味着你必须使用双引号("),而不是单引号(')。

注意:不像shell形式那样,exec形式不能调用一个shell命令,这意味着不能做正常的shell处理过程,例如,RUN [ "echo", "$HOME" ]$HOME上不会做变量替换。如果你想使用shell处理,要么使用shell的形式,要么直接执行一个shell,例如:RUN [ "sh", "-c", "echo $HOME" ]。当使用exec形式直接执行一个shell命令的时候,这和shell形式是一样的,它是一个正在做环境变量扩展的shell,而不是docker。

无论使用的是shell还是exec形式,CMD指令这是的命令都是在运行这个镜像的时候被执行。

如果你使用shell形式的CMD指令,<command>将使用/bin/sh -c执行:

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

如果你想运行<command>,而不是shell,你必须将命令表示成一个JSON数组,而且还要给出命令的全路径。这个数组形式的是CMD的优先格式。任何额外的参数都必须单独表示为数组中的字符串:

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

如果你想每次你的容器都执行同一个命令,你应该考虑将CMD指令结合ENTRYPOINT使用。详见ENTRYPOINT

如果用户给docker run指定参数,它将覆盖CMD指定的默认命令。

注意:不要混淆RUNCMDRUN是实际的执行一个命令,并提交执行结果;CMD指令在编译阶段不会执行任何命令,但是为镜像预制了一个命令。

01.4 LABEL

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

LABEL指令为一个镜像添加一些元数据,一个LABEL是一个键值对。为了能在LABEL的value中包含空格,需要使用双引号,使用反斜杠可以被当做一个命令行解析。下面是一些用法:

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,为了指定多个label,Docker建议尽可能地将所有的label合并到一个LABEL指令中,如果你使用很多label,每个LABEL指令都能产生一个新的层,这回导致镜像是非常低效的,下面这个例子是产生一个镜像层的例子:

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

上面的也可以被写成:

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

Label是叠加的,它会包含FROM的镜像中的Label,如果Docker遇到一个已经存在的Label的key值,新的值会覆盖掉前面同一个key的所有Label值。

查看一个镜像的label可以使用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"
},

01.5 MAINTAINER (过时的)

MAINTAINER <name>

MAINTAINER指令是用来设置制作镜像的作者的,LABEL指令实现这个功能更灵活,你最好使用它来替换MAINTAINER,并且它可以设置任何你需要的元数据,并且容易查看,例如命令docker inspect。你可以这样使用label来对应MAINTAINER字段:

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

这样和其它的label一样是可以使用docker inspect来查看的。

01.6 EXPOSE

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

EXPOSE指令通知Docker容器在运行时监听指定的网络端口,你还可以指定监听的端口是TCP还是UDP,如果不指定协议,默认的是TCP。

EXPOSE指令不会真正发布这个端口,它的作用是在构建镜像和运行容器的用户之间建立一个文档记录,指定哪个端口是被预定发布的。为了能够在运行容器的时候实际发布一个端口,在docker run命令后使用-p标识发布和映射一个或者多个端口,或者使用-p标识发布所有的暴露端口并映射它们到高位端口。

设置端口直接指向主机系统的方法见 -P 标识的使用方法docker network命令支持创建容器之间的网络通信,并不需要发布和暴露指定端口,这是因为通过网络连接的容器,可以使用任何端口互相通信。更多的详细信息参见网络总结

01.7 ENV

ENV <key> <value>
ENV <key>=<value> ...

ENV指令设置环境变量<key>的值<value>,这个值将存在于Dockerfile的所有后代中,也可能被内联替换(replaced inline)。

ENV指令有两种形式,第一种形式是,ENV <key> <value>,它会给一个变量设置一个值,第一个空格之后的整个字符串都将被视作<value>——包含空格和双引号字符。

第二种形式是,ENV <key>=<value> ...,允许一次性设置多个变量,注意,第二种形式在语法上使用了等号,而第一种形式中没有。像一般的命令行解析一样,双引号和反斜杠都可以用在value中包含空格的情况下。

例如:

ENV myName="John Doe" myDog=Rex\ The\ Dog \
    myCat=fluffy

ENV myName John Doe
ENV myDog Rex The Dog
ENV myCat fluffy

它们在最后的镜像中产生相同的结果,但是推荐使用第一种形式,它只产生一个缓存层。

当运行那个产生的镜像的容器的时候,使用ENV设置的环境变量将会被持久化。你可以使用docker inspect查看这些值,使用docker run --env <key>=<value>可以改变它们。

注意:环境变量的持久化可能会导致意外的副作用,例如设置了ENV DEBIAN_FRONTEND noninteractive,可能会混淆基于Debian的镜像的 apt-get 用户。为了给单个命令设置一个环境变量,可以使用RUN <key>=<value> <command>

01.8 ADD

ADD指令同样有两种形式:

  • ADD <src>... <dest>
  • ADD ["<src>",... "<dest>"](如果路径中包含空格,这种形式是必须的)

ADD指令是从<src>中copy文件、目录或者远程文件URL中的文件,然后添加它们到镜像的文件系统中,路径是<dest>

可以指定多个<src>,但是它们如果是文件或者目录的话,必须是编译(编译上下文)原路径的相对路径。

每一个<src>都可以包含通配符,匹配方式使用的是Go语言的文件路径的匹配规则 filepath.Match,例如:

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

<dest>是一个绝对路径,或者是WORKDIR的相对路径,source中指定的文件会被copy到目的的容器中。

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

当添加的文件或者目录的路径中包含特殊字符(例如[])时,你需要根据Go语言的规则对它们进行转义,防止它们被当作是通配符,例如,添加文件名为arr[0].txt的文件。使用下面的方式:

ADD arr[[]0].txt /mydir/    # copy a file named "arr[0].txt" to /mydir/

所有新的文件和目录在创建的时候都必须有一个0的UID和GID。

<src>是一个远程文件URL的情形下,目的地必须有600的权限。如果检索到的远程文件有一个HTTP的Last-Modified头信息,头信息的时间戳会被设置到目的文件的mtime。然而,ADD就像其他的任何文件处理过程一样,mtime不包括在文件是否已经改变,缓存是否被更新。

注意:如果你通过标准输入传递一个Dockerfile文件(docker build - < somefile),这时是没有编译上下文的,因此Dockerfile中的ADD指令仅能包含一个URL。你也可以通过标准输入传递一个压缩包(docker build - < archive.tar.gz),Dockerfile在压缩包的根目录,压缩包的其余部分作为编译的上下文。

注意:如果你的URL和文件是有权限保护的,你需要使用RUN wgetRUN curl或者使用容器内的其他工具,因为ADD指令不支持授权。

注意:如果<src>的内容已经改变的话,第一个遇到的ADD指令将会使下面来自Dockerfile的所有指令的缓存失效,这也包括RUN指令的缓存。更多信息参见Dockerfile的最佳实践指导

ADD遵循下列规则:

  • <src>的路径必须在编译的上下文中,你不能使用ADD ../something /something,因为docker build的第一步是发送目录上下文(包括子目录)到docker的守护进程。

  • 如果<src>是一个URL,<dest>不以斜杠结尾,那么从URL上下载的文件会被copy到<dest>

  • 如果<src>是一个URL,<dest>以斜杠结尾,那么URL上指定的文件名就是下载的文件名<dest>/<filename>,例如,ADD http://example.com/foobar /将会创建那个文件/foobar。URL必须是一个简单的路径,以便能够发现一个合适的文件名(http://example.com是无效的)。

  • 如果<src>是一个目录,目录下的所有内容都会被copy,包括文件系统的元数据。

注意:文件目录本身不会被copy的,仅仅是它的内容。

  • 如果<src>是一个本地的 tar 压缩包,使用的是支持的压缩格式(本身,gzip,bzip2 或者 xz),它被解压之后是一个目录。远程URL的资源是不能解压的,无论是一个目录被copy还是被解压的时候,tar -x有相同的动作,结果是:
    1.在目的地无论存在什么
  1. 源的树形结构中内容解决冲突的时候都是优先选择后者,逐个文件的解决。

注意:无论一个文件是否被确定为可识别的压缩格式都会是基于文件内容完成,而不是文件名。例如,如果一个空文件以.tar.gz结尾,将不会被识别为一个压缩文件,不会生成任何的解压错误信息,而是文件只会被简单地copy到目的地。

  • 如果<src>是一个任意其它类型的文件,它和元数据一起都会被copy。如果<dest>以斜杠/结尾,它将被认为是一个目录,<src>的内容将会被写到<dest>/base(<src>)

  • 如果指定了多个<src>资源,不管使用的是目录还是通配符,<dest>必须是一个目录,即它必须以斜杠/结尾。

  • 如果<dest>不以斜杠结尾,它将被认为是一个普通的文件,<src>的内容将会被写到<dest>

  • 如果<dest>不存在,在这个路径所有不存在的目录都会被创建。

01.9 COPY

COPY指令有两种形式:

  • COPY <src>... <dest>
  • COPY ["<src>",... "<dest>"](路径中包含空格的必须使用这种形式)

COPY指令从<src>copy新的文件和目录,然后将它们添加到容器的文件系统中,路径为<dest>

可以指定多个<src>资源,但是它们必须是编译上下文的相对资源目录。

每一个<src>都可以包含通配符,匹配方式使用的是Go语言的文件路径的匹配规则 filepath.Match,例如:

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

<dest>是一个绝对路径,或者是WORKDIR的相对路径,source中指定的文件会被copy到目的的容器中。

COPY test relativeDir/          # adds "test" to `WORKDIR`/relativeDir/
COPY test /absoluteDir/         # adds "test" to /absoluteDir/

当添加的文件或者目录的路径中包含特殊字符(例如[])时,你需要根据Go语言的规则对它们进行转义,防止它们被当作是通配符,例如,copy文件名为arr[0].txt的文件。使用下面的方式:

COPY arr[[]0].txt /mydir/    # copy a file named "arr[0].txt" to /mydir/

所有新的文件和目录在创建的时候都必须有一个0的UID和GID。

注意:如果你通过标准输入传递一个Dockerfile文件(docker build - < somefile),这时是没有编译上下文的,因此COPY指令是不能使用的。

任意的COPY都可以接受一个--from=<name|index>标识,它用于设置资源位置为前面编译的阶段(FROM .. AS <name>创建的),而不使用用户发送的编译上下文。这个标识也接受前面所有编译阶段(以FROM指令开始)分配的数字索引,如果找不到指定名称的编译阶段,则尝试使用同名的镜像。

COPY指令遵循下面的规则:

  • <src>的路径必须在编译的上下文中,不能使用COPY ../something /something,因为docker build的第一步是发送上下文目录(包括子目录)到docker的后台。
  • 如果<src>是一个目录,目录下的所有内容都会被copy,包括文件系统的元数据。

注意:文件目录本身不会被copy的,仅仅是它的内容。

  • 如果<src>是一个任意其它类型的文件,它和元数据一起都会被copy。如果<dest>以斜杠/结尾,它将被认为是一个目录,<src>的内容将会被写到<dest>/base(<src>)

  • 如果指定了多个<src>资源,不管使用的是目录还是通配符,<dest>必须是一个目录,即它必须以斜杠/结尾。

  • 如果<dest>不以斜杠结尾,它将被认为是一个普通的文件,<src>的内容将会被写到<dest>

  • 如果<dest>不存在,在这个路径所有不存在的目录都会被创建。

00.00 暂停一下

docker的文档写的太详细了,全写在一篇文章中太长了,忍痛分割一下吧!余下的部分指令参见《Dockerfile 参考手册(三):指令介绍》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,179评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,229评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,032评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,533评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,531评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,539评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,916评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,813评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,568评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,654评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,354评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,937评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,918评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,152评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,852评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,378评论 2 342

推荐阅读更多精彩内容

  • 转载自 http://blog.opskumu.com/docker.html 一、Docker 简介 Docke...
    极客圈阅读 10,468评论 0 120
  • docker基本概念 1. Image Definition 镜像 Image 就是一堆只读层 read-only...
    慢清尘阅读 8,720评论 1 21
  • 16年12月初,和男友自驾去崇明岛玩了两天,恰天气正好。 在这里要安利上海租车的一种很便捷的方式:EVCARD电动...
    桃子王阅读 1,608评论 0 0
  • 这是一本我没有做功课之后被小比否定的书。。苹果和蝴蝶有什么关系呢? 我说这个苹果从中间切开,是不是很像什么东西?然...
    小比妈妈阅读 942评论 0 0
  • 《为你而生》 作者:子童 1 “轰” 某国小的表彰典礼上,表扬三好学生时,舞台的背景板突然倒了下来。 “啊 ,伟佳...
    林子敬阅读 1,485评论 2 2