.. include:: ../../../sumaccess.rst Dockerfile指令 ================= Dockerfile的基本格式如下: .. code:: dockerfile # Comment INSTRUCTION arguments .. TIP:: 在Dockerfile中,指令(INSTRUCTION)不区分大小写,但是为了与参数区分,推荐大写。 Docker会顺序执行Dockerfile中的指令,第一条指令必须是FROM指令,它用于指定构建镜像的基础镜像。 在Dockerfile中以#开头的行是注释,而在其他位置出现的#会被当成参数,示例如下: .. code:: dockerfile # Comment RUN echo 'Hello World # hello world demo' Dockerfile中的指令有 ``FROM`` 、``MAINTAINER`` 、``RUN`` 、``CMD`` 、``EXPOSE`` 、 ``ENV`` 、``ADD`` 、``COPY`` 、``ENTRYPOINT`` 、``VOLUME`` 、``USER`` 、``WORKDIR`` 、``ONBUILD`` , 错误的指令会被忽略。下面将详细讲解一些重要的Docker指令。 ``FROM`` ----------- 所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。就像我们之前运行了一个 ``nginx`` 镜像的容器,再进行修改一样,基础镜像是必须指定的。而 ``FROM`` 就是指定 **基础镜像**\ ,因此一个 ``Dockerfile`` 中 ``FROM`` 是必备的指令,并且必须是第一条指令。 在 DockerHub上有非常多的高质量的官方镜像,有可以直接拿来使用的服务类的镜像,如 ``nginx`` 、 ``redis`` 、 ``mongo`` 、``mysql`` 、 ``httpd`` 、 ``php`` 、 ``tomcat`` 等; 也有一些方便开发、构建、运行各种语言应用的镜像,如 ``node`` 、 ``openjdk`` 、 ``python`` 、 ``ruby`` 、 ``golang`` 等。可以在其中寻找一个最符合我们最终目标的镜像为基础镜像进行定制。 如果没有找到对应服务的镜像,官方镜像中还提供了一些更为基础的操作系统镜像,如 ``ubuntu`` 、 ``debian`` 、 ``centos`` 、 ``fedora`` 、 ``alpine`` 等,这些操作系 统的软件库为我们提供了更广阔的扩展空间。 除了选择现有镜像为基础镜像外,Docker 还存在一个特殊的镜像,名为 ``scratch``\ 。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。 .. code:: dockerfile FROM scratch ... 如果你以 ``scratch`` 为基础镜像的话,意味着你不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。 不以任何系统为基础,直接将可执行文件复制进镜像的做法并不罕见,比如 `swarm `__\ 、\ `etcd `__\ 。对于 Linux 下静态编译的程序来说,并不需要有操作系统提供运行时支持,所需的一切库都已经在可执行文件里了,因此直接 ``FROM scratch`` 会让镜像体积更加小巧。使用 `Go 语言 `__ 开发的应用很多会使用这种方式来制作镜像,这也是为什么有人认为 Go 是特别适合容器微服务架构的语言的原因之一。 ``COPY`` ------------- 格式: .. code:: dockerfile COPY COPY 指令将从构建上下文目录中 <源路径> 的文件/目录复制到新的一层的镜像内的 <目标路径> 位置。比如: .. code:: dockerfile COPY package.json /usr/src/app/ <源路径> 可以是多个,甚至可以是通配符,其通配符规则要满足 Go的 `filepath.Match `__ 规则,如: .. code:: dockerfile COPY hom* /mydir/ COPY hom?.txt /mydir/ <目标路径> 可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用 ``WORKDIR`` 指令来指定)。 目标路径 **不需要** 事先创建,如果目录不存在会在复制文件前先行创建缺失目录。 .. NOTE:: - 若以反斜杠/结尾则其指向的是目录;否则指向文件。同理。 - 若是一个文件,则的内容会被写入到中; - 否则所指向的文件或目录中的内容会被复制添加到目录中。 - 当指定多个源时,必须是目录。另外,如果不存在,则路径中不存在的目录会被创建。 此外,还需要注意一点,使用 COPY 指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。 这个特性对于镜像定制很有用。特别是构建相关文件都在使用 Git 进行管理的时候。 在使用该指令的时候还可以加上 ``--chown=:`` 选项来改变文件的所属用户及所属组。 .. code:: dockerfile COPY --chown=55:mygroup files* /mydir/ COPY --chown=bin files* /mydir/ COPY --chown=1 files* /mydir/ COPY --chown=10:11 files* /mydir/ ``ADD`` ----------- ``ADD`` 指令和 ``COPY`` 的格式和性质基本一致。但是在 ``COPY`` 基础上增加了一些功能。 比如 ``<源路径>`` 可以是一个 ``URL``\ ,这种情况下,Docker引擎会试图去下载这个链接的文件放到 ``<目标路径>`` 去。 下载后的文件权限自动设置为 ``600``\ ,如果这并不是想要的权限,那么还需要增加额外的一层 ``RUN`` 进行权限调整, 另外,如果下载的是个压缩包,需要解压缩,也一样还需要额外的一层 ``RUN`` 指令进行解压缩。 所以不如直接使用 ``RUN`` 指令,然后使用 ``wget`` 或者 ``curl`` 工具下载,处理权限、解压缩、然后清理无用文件更合理。 因此,这个功能其实并不实用,而且不推荐使用。 如果 ``<源路径>`` 为一个 ``tar`` 压缩文件的话,压缩格式为 ``gzip``, ``bzip2`` 以及 ``xz`` 的情况下,\ ``ADD`` 指令将会自动解压缩这个压缩文件到 ``<目标路径>`` 去。 在某些情况下,这个自动解压缩的功能非常有用,比如官方镜像 ``ubuntu`` 中: .. code:: docker FROM scratch ADD ubuntu-xenial-core-cloudimg-amd64-root.tar.gz / ... 但在某些情况下,如果我们真的是希望复制个压缩文件进去,而不解压缩,这时就不可以使用 ``ADD`` 命令了。 在 Docker 官方的 Dockerfile 最佳实践文档 中要求,尽可能的使用 ``COPY``\ ,因为 ``COPY`` 的语义很明确,就是复制文件而已,而 ``ADD`` 则包含了更复杂的功能,其行为也不一定很清晰。最适合使用 ``ADD`` 的场合,就是所提及的需要自动解压缩的场合。 另外需要注意的是,\ ``ADD`` 指令会令镜像构建缓存失效,从而可能会令镜像构建变得比较缓慢。 因此在 ``COPY`` 和 ``ADD`` 指令中选择的时候,可以遵循这样的原则,所有的文件复制均使用 ``COPY`` 指令,仅在需要自动解压缩的场合使用 ``ADD``\ 。 在使用该指令的时候还可以加上 ``--chown=:`` 选项来改变文件的所属用户及所属组。 .. code:: docker ADD --chown=55:mygroup files* /mydir/ ADD --chown=bin files* /mydir/ ADD --chown=1 files* /mydir/ ADD --chown=10:11 files* /mydir/ ``RUN`` ---------- RUN指令有两种格式: .. code:: docker RUN (shell格式) RUN ["executable", "param1", "param2"](exec格式,推荐格式) RUN指令会在前一条命令创建出的镜像的基础上创建一个容器,并在容器中运行命令,在命令结束运行后提交容器为新镜像, 新镜像被Dockerfile中的下一条指令使用。 RUN指令的两种格式表示命令在容器中的两种运行方式。当使用shell格式时,命令通过/bin/sh-c运行; 当使用exec格式时,命令是直接运行的,容器不调用shell程序,即容器中没有shell程序。 exec格式中的参数会当成JSON数组被Docker解析,故必须使用双引号而不能使用单引号。因为exec格式不会在shell中执行, 所以环境变量的参数不会被替换,例如,当执行RUN [ "echo","$HOME" ]指令时,$HOME不会做变量替换。 如果希望运行shell程序,指令可以写成CMD [ "sh", "-c","echo", "$HOME" ]。 ``CMD`` ----------- CMD指令有3种格式: .. code:: docker CMD (shell格式 CMD ["executable", "param1", "param2"](exec格式,推荐格式) CMD ["param1", "param2"](为ENTRYPOINT指令提供参数) 之前介绍容器的时候曾经说过,Docker 不是虚拟机,容器就是进程。既然是进程,那么在启动容器的时候,需要指定所运行的程序及参数。 ``CMD`` 指令就是用于指定默认的容器主进程的启动命令的。 CMD指令提供容器运行时的默认值,这些默认值可以是一条指令,也可以是一些参数。 **一个Dockerfile中可以有多条CMD指令, 但只有最后一条CMD指令有效** 。 在运行时可以指定新的命令来替代镜像设置中的这个默认命令,比如,ubuntu 镜像默认的 CMD 是 ``/bin/bash`` , 如果我们直接 ``docker run -it ubuntu`` 的话,会直接进入 bash。我们也可以在运行时指定运行别的命令, 如 ``docker run -it ubuntu cat /etc/os-release`` 。这就是用 ``cat /etc/os-release`` 命令替换了默认的 ``/bin/bash`` 命令了,输出了系统版本信息。 在指令格式上,一般推荐使用 ``exec`` 格式,这类格式在解析时会被解析为 JSON 数组,因此一定要使用双引号 ",而不要使用单引号。 如果使用 shell 格式的话,实际的命令会被包装为 ``sh -c`` 的参数的形式进行执行。比如: .. code:: Dockerfile CMD echo $HOME 在实际执行中,会将其变更为: .. code:: Dockerfile CMD [ "sh", "-c", "echo $HOME" ] .. TIP:: 这就是为什么我们可以使用环境变量的原因,因为这些环境变量会被 shell 进行解析处理。 提到 ``CMD`` 就不得不提容器中应用在前台执行和后台执行的问题。这是初学者常出现的一个混淆。 Docker 不是虚拟机,容器中的应用都应该以前台执行,而不是像虚拟机、物理机里面那样,用 ``systemd`` 去启动后台服务,容器内没有后台服务的概念。 一些初学者将 ``CMD`` 写为: .. code:: Dockerfile CMD service nginx start 然后发现容器执行后就立即退出了。甚至在容器内去使用 ``systemctl`` 命令结果却发现根本执行不了。这就是因为没有搞明白前台、 后台的概念,没有区分容器和虚拟机的差异,依旧在以传统虚拟机的角度去理解容器。 对于容器而言,其启动程序就是容器应用进程,容器就是为了主进程而存在的,主进程退出,容器就失去了存在的意义,从而退出, 其它辅助进程不是它需要关心的东西。 而使用 ``service nginx start`` 命令,则是希望 upstart 来以后台守护进程形式启动 ``nginx`` 服务。而刚才说了 ``CMD service nginx start`` 会被理解为 ``CMD [ "sh", "-c", "service nginx start"]``\ ,因此主进程实际上是 ``sh``\ 。那么当 ``service nginx start`` 命令结束后,\ ``sh`` 也就结束了,\ ``sh`` 作为主进程退出了,自然就会令容器退出。 正确的做法是直接执行 ``nginx`` 可执行文件,并且要求以前台形式运行。比如: .. code:: docker CMD ["nginx", "-g", "daemon off;"] ``ENTRYPOINT`` ----------------- ENTRYPOINT指令有两种格式: .. code:: ENTRYPOINT (shell格式) ENTRYPOINT ["executable", "param1", "param2"](exec格式,推荐格式) ENTRYPOINT指令和CMD指令类似,都可以让容器在每次启动时执行相同的命令,但它们之间又有不同。 一个Dockerfile中可以有多条ENTRYPOINT指令,但只有最后一条ENTRYPOINT指令有效。 **当使用shell格式时, ENTRYPOINT指令会忽略任何CMD指令和docker run命令的参数** ,并且会运行在 ``/bin/sh -c`` 中。 这意味着ENTRYPOINT指令进程为 ``/bin/sh -c`` 的子进程,进程在容器中的PID将不是1,且不能接受Unix信号。 即当使用``docker stop `` 命令时,命令进程接收不到SIGTERM信号。 我们推荐使用exec格式, **使用此格式时,``docker run`` 传入的命令参数会覆盖CMD指令的内容并且附加到ENTRYPOINT指令的参数中** 。 从ENTRYPOINT的使用中可以看出,CMD可以是参数,也可以是指令,而ENTRYPOINT只能是命令; 另外,docker run命令提供的运行命令参数可以覆盖CMD,但不能覆盖ENTRYPOINT。 使用场景1: 让镜像变成像命令一样使用 +++++++++++++++++++++++++++++++++++++++++ 假设我们需要一个得知自己当前公网 IP 的镜像,那么可以先用 ``CMD`` 来实现: .. code:: FROM ubuntu:18.04 RUN apt-get update \ && apt-get install -y curl \ && rm -rf /var/lib/apt/lists/* CMD [ "curl", "-s", "http://myip.ipip.net" ] 使用 ``docker build -t myip:v0.1`` . 来构建镜像的话 .. code:: bash $ docker build -t myip:v0.1 . Sending build context to Docker daemon 2.048kB Step 1/3 : FROM ubuntu:18.04 ---> 81bcf752ac3d Step 2/3 : RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* ---> Using cache ---> 317d985ee7c5 Step 3/3 : CMD ["curl", "-s", "http://myip.ipip.net"] ---> Using cache ---> bea0bd84bb29 Successfully built bea0bd84bb29 Successfully tagged myip:v0.1 如果我们需要查询当前公网 ``IP`` ,只需要执行: .. code:: $ docker run --rm myip:v0.1 当前 IP:xx.xx.xx.xx 来自于:中国 江苏 无锡 移动 嗯,这么看起来好像可以直接把镜像当做命令使用了,不过命令总有参数,如果我们希望加参数呢? 比如从上面的 ``CMD`` 中可以看到实质的命令是 ``curl`` ,那么如果我们希望显示 HTTP 头信息,就需要加上 ``-i`` 参数。 那么我们可以直接加 ``-i`` 参数给 ``docker run myip:v0.1`` 么? .. code:: $ docker run --rm myip:v0.1 -i docker: Error response from daemon: OCI runtime create failed: container_linux.go:349: starting container process caused "exec: \"-i\": executable file not found in $PATH": unknown. 我们可以看到可执行文件找不到的报错,``executable file not found`` 。之前我们说过,跟在镜像名后面的是 ``command`` , 运行时会替换 CMD 的默认值。因此这里的 ``-i`` 替换了原来的 ``CMD`` ,而不是添加在原来的 ``curl -s http://myip.ipip.net`` 后面。而 ``-i`` 根本不是命令,所以自然找不到。 那么如果我们希望加入 -i 这参数,我们就必须重新完整的输入这个命令: .. code:: $ docker run --rm myip:v0.1 curl -s -i http://myip/ipip.net 这显然不是很好的解决方案,而使用 ENTRYPOINT 就可以解决这个问题。现在我们重新用 ENTRYPOINT 来实现这个镜像: .. code-block:: dockerfile :linenos: :emphasize-lines: 5 FROM ubuntu:18.04 RUN apt-get update \ && apt-get install -y curl \ && rm -rf /var/lib/apy/lists/* ENTRYPOINT ["curl", "-s", "http://myip.ipip.net"] 接下来再看下docker run命令行追加CMD的效果: .. code:: bash $ docker run --rm myip:v0.2 当前 IP:36.155.108.159 来自于:中国 江苏 无锡 移动 $ docker run --rm myip:v0.2 -i HTTP/1.1 200 OK Date: Fri, 28 May 2021 04:49:06 GMT Content-Type: text/plain; charset=utf-8 Content-Length: 69 Connection: keep-alive X-Via-JSL: 2633f0d,- Set-Cookie: __jsluid_h=9aad71a669b267ebe5432972f41619d3; max-age=31536000; path=/; HttpOnly X-Cache: bypass 当前 IP:36.155.108.159 来自于:中国 江苏 无锡 移动 可以看到,这次成功了。这是因为当存在 ENTRYPOINT 后,CMD 的内容将会作为参数传给 ``ENTRYPOINT`` ,而这里 ``-i`` 就是新的 ``CMD`` ,因此会作为参数传给 ``curl`` ,从而达到了我们预期的效果。 使用场景2: 应用运行前的准备工作 +++++++++++++++++++++++++++++++++++++++++ 启动容器就是启动主进程,但有些时候,启动主进程前,需要一些准备工作。 比如 ``mysql`` 类的数据库,可能需要一些数据库配置、初始化的工作,这些工作要在最终的 mysql 服务器运行之前解决。 此外,可能希望避免使用 ``root`` 用户去启动服务,从而提高安全性,而在启动服务前还需要以 ``root`` 身份执行一些必要的准备工作,最后切换到服务用户身份启动服务。或者除了服务外,其它命令依旧可以使用 ``root`` 身份执行,方便调试等。 这些准备工作是和容器 ``CMD`` 无关的,无论 ``CMD`` 为什么,都需要事先进行一个预处理的工作。这种情况下,可以写一个脚本,然后放入 ``ENTRYPOINT`` 中去执行,而这个脚本会将接到的参数(也就是 ````\ )作为命令,在脚本最后执行。比如官方镜像 ``redis`` 中就是这么做的: .. code:: docker FROM alpine:3.4 ... RUN addgroup -S redis && adduser -S -G redis redis ... ENTRYPOINT ["docker-entrypoint.sh"] EXPOSE 6379 CMD [ "redis-server" ] 可以看到其中为了 redis 服务创建了 redis 用户,并在最后指定了 ``ENTRYPOINT`` 为 ``docker-entrypoint.sh`` 脚本。 .. code:: shell #!/bin/sh ... # allow the container to be started with `--user` if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then find . \! -user redis -exec chown redis '{}' + exec gosu redis "$0" "$@" fi exec "$@" 该脚本的内容就是根据 ``CMD`` 的内容来判断,如果是 ``redis-server`` 的话,则切换到 ``redis`` 用户身份启动服务器,否则依旧使用 ``root`` 身份执行。比如: .. code:: shell $ docker run -it redis id uid=0(root) gid=0(root) groups=0(root) ``WORKDIR`` -------------- 格式为 ``WORKDIR <工作目录路径>``\ 。 使用 ``WORKDIR`` 指令可以来指定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录,如该目录不存在,\ ``WORKDIR`` 会帮你建立目录。 之前提到一些初学者常犯的错误是把 ``Dockerfile`` 等同于 Shell 脚本来书写,这种错误的理解还可能会导致出现下面这样的错误: .. code:: dockerfile RUN cd /app RUN echo "hello" > world.txt 如果将这个 ``Dockerfile`` 进行构建镜像运行后,会发现找不到 ``/app/world.txt`` 文件,或者其内容不是 ``hello``\ 。原因其实很简单,在 Shell 中,连续两行是同一个进程执行环境,因此前一个命令修改的内存状态,会直接影响后一个命令;而在 ``Dockerfile`` 中,这两行 ``RUN`` 命令的执行环境根本不同,是两个完全不同的容器。这就是对 ``Dockerfile`` 构建分层存储的概念不了解所导致的错误。 之前说过每一个 ``RUN`` 都是启动一个容器、执行命令、然后提交存储层文件变更。第一层 ``RUN cd /app`` 的执行仅仅是当前进程的工作目录变更,一个内存上的变化而已,其结果不会造成任何文件变更。而到第二层的时候, 启动的是一个全新的容器,跟第一层的容器更完全没关系,自然不可能继承前一层构建过程中的内存变化。 因此如果需要改变以后各层的工作目录的位置,那么应该使用 ``WORKDIR`` 指令。 .. code:: dockerfile WORKDIR /app RUN echo "hello" > world.txt 如果你的 ``WORKDIR`` 指令使用的相对路径,那么所切换的路径与之前的 ``WORKDIR`` 有关: .. code:: dockerfile FROM ubuntu:18.04 WORKDIR /a WORKDIR b WORKDIR c ENTRYPOINT ["sh", "-c", "pwd"] 构建镜像 .. code:: bash $ docker build -t test_workdir:v0.1 . Sending build context to Docker daemon 5.12kB Step 1/5 : FROM ubuntu:18.04 ---> 81bcf752ac3d Step 2/5 : WORKDIR /a ---> Using cache ---> 35eebab8577d Step 3/5 : WORKDIR b ---> Using cache ---> 23c3ca3b336f Step 4/5 : WORKDIR c ---> Using cache ---> d9dd1b21b8af Step 5/5 : ENTRYPOINT ["sh", "-c", "pwd"] ---> Running in 98e929b4a61e Removing intermediate container 98e929b4a61e ---> d48acbeb472c Successfully built d48acbeb472c Successfully tagged test_workdir:v0.1 运行容器 .. code:: bash $ docker run --rm test_workdir:v0.1 /a/b/c 可见 ``pwd`` 输出的结果为 ``/a/b/c`` 。 ``ENV`` ----------- 格式: .. code:: dockerfile ENV 或ENV = ... ENV指令可以为镜像创建出来的容器声明环境变量。并且在Dockerfile中,ENV指令声明的环境变量 会被后面的特定指令(即ENV、ADD、COPY、WORKDIR、EXPOSE、VOLUME、USER)解释使用。 其他指令使用环境变量时,使用格式为 ``$variable_name`` 或者 ``${variable_name}`` 。 在变量前面添加斜杠 ``\`` 可以转义,如 ``\$foo`` 或者 ``\${foo}`` ,将会被分别转换为 ``$foo`` 和 ``${foo}`` ,而不是环境变量所保存的值。另外,ONBUILD指令不支持环境替换。 在官方 node 镜像 Dockerfile 中,就有类似这样的代码: .. code:: dockerfile ENV NODE_VERSION 7.2.0 RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \ && curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \ && gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \ && grep " node-v$NODE_VERSION-linux-x64.tar.xz\$" SHASUMS256.txt | sha256sum -c - \ && tar -xJf "node-v$NODE_VERSION-linux-x64.tar.xz" -C /usr/local --strip-components=1 \ && rm "node-v$NODE_VERSION-linux-x64.tar.xz" SHASUMS256.txt.asc SHASUMS256.txt \ && ln -s /usr/local/bin/node /usr/local/bin/nodejs 在这里先定义了环境变量 ``NODE_VERSION``\ ,其后的 ``RUN`` 这层里,多次使用 ``$NODE_VERSION`` 来进行操作定制。可以看到,将来升级镜像构建版本的时候,只需要更新 ``7.2.0`` 即可,\ ``Dockerfile`` 构建维护变得更轻松了。 下列指令可以支持环境变量展开: ``ADD``\ 、\ ``COPY``\ 、\ ``ENV``\ 、\ ``EXPOSE``\ 、\ ``FROM``\ 、\ ``LABEL``\ 、\ ``USER``\ 、\ ``WORKDIR``\ 、\ ``VOLUME``\ 、\ ``STOPSIGNAL``\ 、\ ``ONBUILD``\ 、\ ``RUN``\ 。 可以从这个指令列表里感觉到,环境变量可以使用的地方很多,很强大。通过环境变量,我们可以让一份 ``Dockerfile`` 制作更多的镜像,只需使用不同的环境变量即可。 ``ARG`` ------------ 格式:\ ``ARG <参数名>[=<默认值>]`` 构建参数和 ``ENV`` 的效果一样,都是设置环境变量。所不同的是,\ ``ARG`` 所设置的构建环境的环境变量,在将来容器运行时是不会存在这些环境变量的。但是不要因此就使用 ``ARG`` 保存密码之类的信息,因为 ``docker history`` 还是可以看到所有值的。 ``Dockerfile`` 中的 ``ARG`` 指令是定义参数名称,以及定义其默认值。该默认值可以在构建命令 ``docker build`` 中用 ``--build-arg <参数名>=<值>`` 来覆盖。 在 1.13 之前的版本,要求 ``--build-arg`` 中的参数名,必须在 ``Dockerfile`` 中用 ``ARG`` 定义过了,换句话说,就是 ``--build-arg`` 指定的参数,必须在 ``Dockerfile`` 中使用了。如果对应参数没有被使用,则会报错退出构建。从 1.13 开始,这种严格的限制被放开,不再报错退出,而是显示警告信息,并继续构建。这对于使用 CI 系统,用同样的构建流程构建不同的 ``Dockerfile`` 的时候比较有帮助,避免构建命令必须根据每个 Dockerfile 的内容修改。 ARG 指令有生效范围,如果在 ``FROM`` 指令之前指定,那么只能用于 ``FROM`` 指令中。 .. code:: dockerfile ARG DOCKER_USERNAME=library FROM ${DOCKER_USERNAME}/alpine RUN set -x ; echo ${DOCKER_USERNAME} 使用上述 Dockerfile 会发现无法输出 ``${DOCKER_USERNAME}`` 变量的值,要想正常输出,你必须在 ``FROM`` 之后再次指定 ``ARG`` .. code:: dockerfile # 只在 FROM 中生效 ARG DOCKER_USERNAME=library FROM ${DOCKER_USERNAME}/alpine # 要想在 FROM 之后使用,必须再次指定 ARG DOCKER_USERNAME=library RUN set -x ; echo ${DOCKER_USERNAME} 对于多阶段构建,尤其要注意这个问题 .. code:: dockerfile # 这个变量在每个 FROM 中都生效 ARG DOCKER_USERNAME=library FROM ${DOCKER_USERNAME}/alpine RUN set -x ; echo 1 FROM ${DOCKER_USERNAME}/alpine RUN set -x ; echo 2 对于上述 Dockerfile 两个 ``FROM`` 指令都可以使用 ``${DOCKER_USERNAME}``\ ,对于在各个阶段中使用的变量都必须在每个阶段分别指定: .. code:: dockerfile ARG DOCKER_USERNAME=library FROM ${DOCKER_USERNAME}/alpine # 在FROM 之后使用变量,必须在每个阶段分别指定 ARG DOCKER_USERNAME=library RUN set -x ; echo ${DOCKER_USERNAME} FROM ${DOCKER_USERNAME}/alpine # 在FROM 之后使用变量,必须在每个阶段分别指定 ARG DOCKER_USERNAME=library RUN set -x ; echo ${DOCKER_USERNAME} ``VOLUME`` --------------------- 格式为: - ``VOLUME ["<路径1>", "<路径2>"...]`` - ``VOLUME <路径>`` 之前我们说过,容器运行时应该尽量保持容器存储层不发生写操作,对于数据库类需要保存动态数据的应用,其数据库文件应该保存于卷(volume)中。为了防止运行时用户忘记将动态文件所保存目录挂载为卷,在 ``Dockerfile`` 中,我们可以事先指定某些目录挂载为匿名卷,这样在运行时如果用户不指定挂载,其应用也可以正常运行,不会向容器存储层写入大量数据。 .. code:: dockerfile VOLUME /data 这里的 ``/data`` 目录就会在运行时自动挂载为匿名卷,任何向 ``/data`` 中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。当然,运行时可以覆盖这个挂载设置。比如: .. code:: shell docker run -d -v mydata:/data xxxx 在这行命令中,就使用了 ``mydata`` 这个命名卷挂载到了 ``/data`` 这个位置,替代了 ``Dockerfile`` 中定义的匿名卷的挂载配置。 ``EXPOSE`` --------------------- 格式为 ``EXPOSE <端口1> [<端口2>...]``\ 。 ``EXPOSE`` 指令是声明运行时容器提供服务端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务。在 Dockerfile 中写入这样的声明有两个好处,一个是帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;另一个用处则是在运行时使用随机端口映射时,也就是 ``docker run -P`` 时,会自动随机映射 ``EXPOSE`` 的端口。 要将 ``EXPOSE`` 和在运行时使用 ``-p <宿主端口>:<容器端口>`` 区分开来。\ ``-p``\ ,是映射宿主端口和容器端口,换句话说,就是将容器的对应端口服务公开给外界访问,而 ``EXPOSE`` 仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行端口映射。 ``USER`` --------------------- 格式:\ ``USER <用户名>[:<用户组>]`` ``USER`` 指令和 ``WORKDIR`` 相似,都是改变环境状态并影响以后的层。\ ``WORKDIR`` 是改变工作目录,\ ``USER`` 则是改变之后层的执行 ``RUN``, ``CMD`` 以及 ``ENTRYPOINT`` 这类命令的身份。 注意,\ ``USER`` 只是帮助你切换到指定用户而已,这个用户必须是事先建立好的,否则无法切换。 .. code:: dockerfile RUN groupadd -r redis && useradd -r -g redis redis USER redis RUN [ "redis-server" ] 如果以 ``root`` 执行的脚本,在执行期间希望改变身份,比如希望以某个已经建立好的用户来运行某个服务进程,不要使用 ``su`` 或者 ``sudo``\ ,这些都需要比较麻烦的配置,而且在 TTY 缺失的环境下经常出错。建议使用 `gosu `__\ 。 .. code:: docker # 建立 redis 用户,并使用 gosu 换另一个用户执行命令 RUN groupadd -r redis && useradd -r -g redis redis # 下载 gosu RUN wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/1.12/gosu-amd64" \ && chmod +x /usr/local/bin/gosu \ && gosu nobody true # 设置 CMD,并以另外的用户执行 CMD [ "exec", "gosu", "redis", "redis-server" ] ``HEALTHCHECK`` --------------------- 格式: - ``HEALTHCHECK [选项] CMD <命令>``\ :设置检查容器健康状况的命令 - ``HEALTHCHECK NONE``\ :如果基础镜像有健康检查指令,使用这行可以屏蔽掉其健康检查指令 ``HEALTHCHECK`` 指令是告诉 Docker 应该如何进行判断容器的状态是否正常,这是 Docker 1.12 引入的新指令。 在没有 ``HEALTHCHECK`` 指令前,Docker 引擎只可以通过容器内主进程是否退出来判断容器是否状态异常。很多情况下这没问题,但是如果程序进入死锁状态,或者死循环状态,应用进程并不退出,但是该容器已经无法提供服务了。在 1.12 以前,Docker 不会检测到容器的这种状态,从而不会重新调度,导致可能会有部分容器已经无法提供服务了却还在接受用户请求。 而自 1.12 之后,Docker 提供了 ``HEALTHCHECK`` 指令,通过该指令指定一行命令,用这行命令来判断容器主进程的服务状态是否还正常,从而比较真实的反应容器实际状态。 当在一个镜像指定了 ``HEALTHCHECK`` 指令后,用其启动容器,初始状态会为 ``starting``\ ,在 ``HEALTHCHECK`` 指令检查成功后变为 ``healthy``\ ,如果连续一定次数失败,则会变为 ``unhealthy``\ 。 ``HEALTHCHECK`` 支持下列选项: - ``--interval=<间隔>``\ :两次健康检查的间隔,默认为 30 秒; - ``--timeout=<时长>``\ :健康检查命令运行超时时间,如果超过这个时间,本次健康检查就被视为失败,默认 30 秒; - ``--retries=<次数>``\ :当连续失败指定次数后,则将容器状态视为 ``unhealthy``\ ,默认 3 次。 和 ``CMD``, ``ENTRYPOINT`` 一样,\ ``HEALTHCHECK`` 只可以出现一次,如果写了多个,只有最后一个生效。 在 ``HEALTHCHECK [选项] CMD`` 后面的命令,格式和 ``ENTRYPOINT`` 一样,分为 ``shell`` 格式,和 ``exec`` 格式。命令的返回值决定了该次健康检查的成功与否:\ ``0``\ :成功;\ ``1``\ :失败;\ ``2``\ :保留,不要使用这个值。 假设我们有个镜像是个最简单的 Web 服务,我们希望增加健康检查来判断其 Web 服务是否在正常工作,我们可以用 ``curl`` 来帮助判断,其 ``Dockerfile`` 的 ``HEALTHCHECK`` 可以这么写: .. code:: dockerfile FROM nginx RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* HEALTHCHECK --interval=5s --timeout=3s \ CMD curl -fs http://localhost/ || exit 1 这里我们设置了每 5 秒检查一次(这里为了试验所以间隔非常短,实际应该相对较长),如果健康检查命令超过 3 秒没响应就视为失败,并且使用 ``curl -fs http://localhost/ || exit 1`` 作为健康检查命令。 使用 ``docker build`` 来构建这个镜像: .. code:: shell $ docker build -t test:v1 . Sending build context to Docker daemon 2.048kB Step 1/3 : FROM nginx:latest ...... ---> 666c5320c9d3 Successfully built 666c5320c9d3 Successfully tagged test:v1 构建好了后,我们启动一个容器: .. code:: shell $ docker run -d --name web -p 8001:80 test:v1 48118ab055f56f3805153f90566115f0e7c1b3dcf8d9f67394b18f5deb942543 当运行该镜像后,可以通过 ``docker container ls`` 看到最初的状态为 ``(health: starting)``\ : .. code:: shell $ docker container ls CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 03e28eb00bd0 myweb:v1 "nginx -g 'daemon off" 3 seconds ago Up 2 seconds (health: starting) 80/tcp, 443/tcp web 在等待几秒钟后,再次 ``docker container ls``\ ,就会看到健康状态变化为了 ``(healthy)``\ : .. code:: shell $ docker container ls CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 03e28eb00bd0 myweb:v1 "nginx -g 'daemon off" 18 seconds ago Up 16 seconds (healthy) 80/tcp, 443/tcp web 如果健康检查连续失败超过了重试次数,状态就会变为 ``(unhealthy)``\ 。 为了帮助排障,健康检查命令的输出(包括 ``stdout`` 以及 ``stderr``\ )都会被存储于健康状态里,可以用 ``docker inspect`` 来查看。 .. code:: shell $ docker inspect --format '{{json .State.Health}}' web | python -m json.tool { "FailingStreak": 0, "Log": [ { "End": "2021-05-31T14:57:02.822820903+08:00", "ExitCode": 0, "Output": "\n\n\nWelcome to nginx!\n\n\n\n

Welcome to nginx!

\n

If you see this page, the nginx web server is successfully installed and\nworking. Further configuration is required.

\n\n

For online documentation and support please refer to\nnginx.org.
\nCommercial support is available at\nnginx.com.

\n\n

Thank you for using nginx.

\n\n\n", "Start": "2021-05-31T14:57:02.739461919+08:00" }, { "End": "2021-05-31T14:57:23.159938424+08:00", "ExitCode": 0, "Output": "\n\n\nWelcome to nginx!\n\n\n\n

Welcome to nginx!

\n

If you see this page, the nginx web server is successfully installed and\nworking. Further configuration is required.

\n\n

For online documentation and support please refer to\nnginx.org.
\nCommercial support is available at\nnginx.com.

\n\n

Thank you for using nginx.

\n\n\n", "Start": "2021-05-31T14:57:23.082654997+08:00" } ], "Status": "healthy" } ``ONBUILD`` ---------------------------- 格式:\ ``ONBUILD <其它指令>``\ 。 ``ONBUILD`` 是一个特殊的指令,它后面跟的是其它指令,比如 ``RUN``, ``COPY`` 等,而这些指令,在当前镜像构建时并不会被执行。只有当以当前镜像为基础镜像,去构建下一级镜像的时候才会被执行。 ``Dockerfile`` 中的其它指令都是为了定制当前镜像而准备的,唯有 ``ONBUILD`` 是为了帮助别人定制自己而准备的。 编写父镜像``Dockerfile``\ : .. code:: docker FROM ubuntu:latest MAINTAINER father-image ONBUILD RUN apt-get update && apt-get install tree -y && rm -rf /var/lib/apt/lists/* ONBUILD RUN mkdir mydir 构建镜像为father: .. code:: shell $ docker build -t father:v1 . Sending build context to Docker daemon 2.048kB Step 1/4 : FROM ubuntu:latest latest: Pulling from library/ubuntu 345e3491a907: Pull complete 57671312ef6f: Pull complete 5e9250ddb7d0: Pull complete Digest: sha256:adf73ca014822ad8237623d388cedf4d5346aa72c270c5acc01431cc93e18e2d Status: Downloaded newer image for ubuntu:latest ---> 7e0aa2d69a15 Step 2/4 : MAINTAINER father-image ---> Running in 87b6c6596149 Removing intermediate container 87b6c6596149 ---> 3de0b4ab00e7 Step 3/4 : ONBUILD RUN apt-get update && apt-get install tree -y && rm -rf /var/lib/apt/lists/* ---> Running in a0ff4fab64db Removing intermediate container a0ff4fab64db ---> 731c21056063 Step 4/4 : ONBUILD RUN mkdir mydir ---> Running in 5b59a0d0a00d Removing intermediate container 5b59a0d0a00d ---> ee17bc9c194a Successfully built ee17bc9c194a Successfully tagged father:v1 并创建容器: .. code:: bash $ docker run -it --rm father:v1 root@012b7f578bae:/# tree bash: tree: command not found root@012b7f578bae:/# ls / bin dev etc home lib lib64 lost+found media mnt opt proc root run sbin srv sys tmp usr var 发现此时由父镜像启动的容器是没有tree命令的,根目录下也没有mydir目录。说明ONBUILD后面的指令并不会在自己的构建中执行。 编写子镜像 ``Dockerfile`` : .. code:: dockerfile FROM father:v1 MAINTAINER son 构建son镜像: .. code:: bash $ docker build -t son:v1 . 根据镜像启动容器: .. code:: bash $ docker run --rm -it son:v1 root@733473ae9daa:/# ls bin dev home lib32 libx32 mnt opt root sbin sys usr boot etc lib lib64 media mydir proc run srv tmp var root@733473ae9daa:/# tree /opt /opt 0 directories, 0 files 在实际工作中,利用ONBUILD指令,通常用于创建一个模板镜像,后续可以根据该模板镜像创建特定的子镜像,需要在子镜像构建过程中执行的一些通用操作就可以在模板镜像对应的dockerfile文件中用ONBUILD指令指定。 从而减少dockerfile文件的重复内容编写。 ``LABEL`` ---------------------------- ``LABEL`` 指令用来给镜像以键值对的形式添加一些元数据(metadata)。 .. code:: docker LABEL = = = ... 我们还可以用一些标签来申明镜像的作者、文档地址等: .. code:: docker LABEL org.opencontainers.image.authors="yeasy" LABEL org.opencontainers.image.documentation="https://yeasy.gitbooks.io" 具体可以参考 https://github.com/opencontainers/image-spec/blob/master/annotations.md ``SHELL`` ---------------------------- 格式:\ ``SHELL ["executable", "parameters"]`` SHELL` 指令可以指定 `RUN``ENTRYPOINT``CMD` 指令的 shell,Linux 中默认为 `["/bin/sh", "-c"] SHELL ["/bin/sh", "-c"] .. code:: RUN lll ; ls SHELL ["/bin/sh", "-cex"] RUN lll ; ls 两个 ``RUN`` 运行同一命令,第二个 ``RUN`` 运行的命令会打印出每条命令并当遇到错误时退出。 当 :literal:`ENTRYPOINT``CMD` 以 shell 格式指定时,\ ``SHELL`` 指令所指定的 shell 也会成为这两个指令的 shell .. code:: docker SHELL ["/bin/sh", "-cex"] # /bin/sh -cex "nginx" ENTRYPOINT nginx SHELL ["/bin/sh", "-cex"] # /bin/sh -cex "nginx" CMD nginx .. NOTE:: 参考资料 - ``Dockerfie`` 官方文档:\ https://docs.docker.com/engine/reference/builder/ - ``Dockerfile`` 最佳实践文档:\ https://docs.docker.com/develop/develop-images/dockerfile_best-practices/ - ``Docker`` 官方镜像 ``Dockerfile``\ :\ https://github.com/docker-library/docs .. include:: ../../../comment.rst