dockerfile指令总结

-- TOC --

用dockerfile制作image,是被推荐的方式,对比docker commit,使用dockerfile的好处是image制作过程透明,可重复,image体积更小。

Docker不是虚拟机,容器就是进程!

dockerfile中的每一条指令,都会使image增加一层,据说image最多127层。

image构建时的context

docker时C/S架构,所有docker命令都是发到docker daemon进程去执行,在使用 docker build 的时候,需要指定一个上下文context,这个context中的所有内容会打包传递给docker daemon进程去执行构建image的任务。因此,dockerfile中的源路径,都是以此context为基础的相对路径!在此contex中,可以使用 .dockerignore 文件来排除一些内容给docker daemon。

FROM

dockerfile的第一条指令,FROM base_image,说明此image的基础image,基础镜像。

在 Docker Hub 上有非常多的高质量的官方镜像,有可以直接拿来使用的服务类的镜像,如 nginx、redis、mongo、mysql、httpd、php、tomcat 等;也有一些方便开发、构建、运行各种语言应用的镜像,如 node、openjdk、python、ruby、golang 等。可以在其中寻找一个最符合我们最终目标的镜像为基础镜像进行定制。如果没有找到对应服务的镜像,官方镜像中还提供了一些更为基础的操作系统镜像,如 ubuntu、debian、centos、fedora、alpine 等,这些操作系统的软件库为我们提供了更广阔的扩展空间。

或者 FROM scratch,不要基础镜像!如下是docker官方hello-world的dockerfile(https://github.com/docker-library/hello-world/blob/master/amd64/hello-world/Dockerfile):

FROM scratch
COPY hello /
CMD ["/hello"]

RUN

RUN用来执行shell命令,这些命令一般都会改变文件系统的内容。由于RUN指令太过常用,而每次RUN都会使image的文件系统增加一层,为了适当减少层次数量,一般用一个RUN,后面连续跟多条shell命令。每一个 RUN 都是启动一个容器、执行命令、然后提交存储层文件变更!

RUN的两种格式:

RUN <cmd>  # shell style
RUN ["可执行文件", "参数1", "参数2"...]  # double quote,exec style

看到一种解释:RUN <cmd>这种形式,实在shell中启动cmd,而另一种形式,RUN ["可执行文件", "参数1", "参数2"...],是直接启动可执行文件,他们分别成为shell与exec形式!

一个RUN后面多条指令举例:

RUN set -ex; buildDeps='gcc libc6-dev make wget' \
    && apt-get update \
    && apt-get install -y $buildDeps \
    && wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz" \
    && mkdir -p /usr/src/redis \
    && tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \
    && make -C /usr/src/redis \
    && make -C /usr/src/redis install \
    && rm -rf /var/lib/apt/lists/* \
    && rm redis.tar.gz \
    && rm -r /usr/src/redis \
    && apt-get purge -y --auto-remove $buildDeps

用到了 \ 换行符号,以及 && 命令与,还可以一行多个命令,用分号隔开。上例中后面一组命令是在做删除,删除无用的文件,减少生成image的体积。

在撰写 Dockerfile 的时候,要经常提醒自己,这并不是在写 Shell 脚本,而是在定义每一层该如何构建。

WORKDIR

指定后续dockerfile指令的工作路径,如果路径不存在,则创建!如果你的 WORKDIR 指令使用的相对路径,那么所切换的路径与之前的 WORKDIR 有关:

WORKDIR /a
WORKDIR b
WORKDIR c
RUN cmd

则cmd的工作路径为 /a/b/c

不要用RUN中执行cd的方式切换路径,除非它们在一个RUN中!

COPY

COPY指令将从构建image的context目录中 <源路径> 的文件/目录复制到新的一层的镜像内的 <目标路径> 位置。<目标路径> 可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用 WORKDIR 指令来指定)。目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。 此外,还需要注意一点,使用 COPY 指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。

ADD

ADD就像COPY,但更复杂,更容易让人迷惑。一般建议拷贝文件直接用COPY,如果想要自动解压,使用ADD:

WORKDIR /xyz/TC
ADD linux-5.4.51.tar.xz linux-5.4.51_x64.config wol.patch /xyz/TC/
RUN cp linux-5.4.51_x64.config linux-5.4.51/.config \
  && cd linux-5.4.51 \
  && patch -p1 --ignore-whitespace < /xyz/TC/wol.patch \
  && make bzImage \
  && make modules

上例使用ADD“处理”了几个文件和一个xz压缩包,这个xz压缩包在ADD到/xyz/TC/路径时,会自动进行解压,因此后面的RUN中,有cd命令不会出错。

CMD

Docker不是虚拟机,容器就是进程。既然是进程,那么在启动容器的时候,需要指定所要运行的程序及参数。CMD 指令就用于指定默认的容器主进程的启动命令。CMD 指令的格式和 RUN 相似,也有相同的两种格式。

在创建启动容器的时候,可以在 docker run 命令行中指定另外一个程序去覆盖CMD指令指定的程序。如果启动容器的进程退出,容器也就退出了,因此不要用CMD指令去启动后台运行的进程,主进程退出,container stop!

ENTRYPOINT

ENTRYPOINT也是用来指定容器执行的进程,与CMD不一样的是,如果使用ENTRYPOINT指定启动进程,在docker run命令行中,还可以动态增加启动进程的执行参数,而CMD指定的启动进程则没有这个性质,容器名称后面的输入,会被认为是用来替换CMD的指令。

ENTRYPOINT也可以在命令行中被替换,需要使用 --entrypoint

当同时存在ENTRYPOINT和CMD这两条指令的时候,容器的启动进程将以如下方式运行:

<ENTRYPOINT> "<CMD>"

有了ENTRYPOINT,容器的启动方式更加灵活多样。

dockerfile中如果出现多个CMD或ENTRYPOINT,只有最后一个生效!

有这样的教程:使用ENTRYPOINT来执行要执行的程序,用CMD来设置默认参数,默认参数可以在docker run的时候用命令行覆盖。

USER

USER 指令和 WORKDIR 相似,都是改变环境状态并影响以后的层。WORKDIR 是改变工作目录,USER 则是改变之后层的执行 RUN, CMD 以及 ENTRYPOINT 这类命令的身份。 注意,USER 只是帮助你切换到指定用户而已,这个用户必须是事先建立好的,否则无法切换。

SHELL

SHELL 指令可以指定 RUN ENTRYPOINT CMD 指令的 shell,Linux 中默认为 ["/bin/sh", "-c"]

当ENTRYPOINT和CMD的指令参数以 shell 格式指定时,SHELL 指令所指定的 shell 及其参数,也会成为这两个指令的执行shell:

SHELL ["/bin/sh", "-cex"]
ENTRYPOINT cmd1  # /bin/sh -cex cmd1
CMD cmd2  # /bin/sh -cex cmd2

LABEL

The LABEL instruction adds metadata to an image. A LABEL is a key-value pair. To include spaces within a LABEL value, use quotes and backslashes as you would in command-line parsing.

LABEL给image增加metadata,metadata是key-value对,可以像在命令行输入一样,用引号把空格括起来,用backslash连接多行。

An image can have more than one label. You can specify multiple labels on a single line.

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 multi.label1="value1" multi.label2="value2" other="value3"
LABEL multi.label1="value1" \
      multi.label2="value2" \
      other="value3"

查看image的label,使用 docker image inspect

$ sudo docker inspect --format='' myimage
{
  "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

按照官方文档描述,建议使用LABEL maintainer="xxx"代替MAINTAINER xxx.

ENV

ENV用来设置环境变量。

ENV <key> <value>
ENV <key1>=<value1> <key2>=<value2>...

ARG

ARG用来设置image构建参数,跟ENV很相似,不同之处在于:

  1. ARG设置的变量,在容器运行时不会存在;
  2. ARG指令的设置,可以在 docker build 时用 --build-arg 来覆盖。
  3. ARG 指令有生效范围,如果在 FROM 指令之前指定,那么只能用于 FROM 指令中。

EXPOSE

EXPOSE在dockerfile中暴露出容器将要提供服务所开放的端口。这只是一个声明,在容器运行时并不会因为这个声明应用就会开启这个端口的服务。在 Dockerfile 中写入这样的声明有两个好处,一个是帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;另一个用处则是在运行时使用随机端口映射时,也就是 docker run -P 时,会自动随机映射 EXPOSE 的端口。

要将 EXPOSE 和在运行时使用 -p <宿主端口>:<容器端口> 区分开来。-p,是映射宿主端口和容器端口,换句话说,就是将容器的对应端口服务公开给外界访问,而 EXPOSE 仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行端口映射。

VOLUME

容器内的进程负责计算,数据的输出放在容器外面,数据不写入容器,数据与容器分离,删除容器不影响数据。要实现这样的机制,就需要定义VOLUME。

VOLUMN这个概念我觉得应该来自硬盘,在Windows系统下,一个分区在逻辑上就是要给volumn,或者多个分区,设置来自多个硬盘的多个分区,在逻辑上组合成一个volumn。简单理解,volumn就是一个用来存放数据的逻辑空间!

通过docker run命令的-v参数创建的volumn挂载点只对创建的容器有效。通过dockerfile的 VOLUME 指令可以在镜像中创建挂载点,这样只要通过该镜像创建的容器都有了挂载点。还有一个区别是,通过 VOLUME 指令创建的挂载点,无法指定宿主机上对应的目录,是自动生成的,用 docker inspect 命令可以查看,在 “Mounts” 的 "Source" 字段。

VOLUME ["/data1","/data2"]

上例,指定两个volumn挂载点,image生成的容器在运行过程中,输出的数据写入这两个位置,等效于写入自动生成的宿主机对应的位置。

使用 docker run 命令创建容器的时候,可以用 --volumes-from 参数,实现多个容器共享宿主机的挂载点,貌似前提是这多个容器来自相同的image。

ONBUILD

ONBUILD 是一个特殊的指令,它后面跟的是其它指令,比如 RUN, COPY 等,而这些指令,在当前镜像构建时并不会被执行。只有当以当前镜像为基础镜像,去构建下一级镜像的时候才会被执行。 Dockerfile 中的其它指令都是为了定制当前镜像而准备的,唯有 ONBUILD 是为了帮助别人定制自己而准备的。

这个指令的使用场景,image主体相同,但不同项目的context有差异,dockerfile中的部分指令,需要在各个不同项目独立构建image的时候再执行。

HEALTHCHECK

容器退出的充要条件是容器内的进程执行结束并退出,但有的时候,容器内的进程一直在执行,没有退出,但它的状态其实已经不对,比如死循环bug,或因某些原因,不再相应请求。此时,容器的状态依然是up。如果容器的image有HEALTHCHECK,通过这种check,可以发现容器的状态异常,并将其状态标记为unhealthy,以便进行别的操作。(查看运行容器的状态 docker ps

当在一个镜像指定了 HEALTHCHECK 指令后,用其启动容器,初始状态会为 starting,在 HEALTHCHECK 指令检查成功后变为 healthy,如果连续一定次数失败,则会变为 unhealthy。

HEALTHCHECK并不是必须的,按情况,如果是一个web server的容器,当用户不能访问网页的时候,自然就异常了。

HEALTHCHECK 支持下列选项:

和 CMD, ENTRYPOINT 一样,HEALTHCHECK 只可以出现一次,如果写了多个,只有最后一个生效。

在 HEALTHCHECK [选项] CMD 后面的命令,格式和 ENTRYPOINT 一样,分为 shell 格式,和 exec 格式。命令的返回值决定了该次健康检查的成功与否:0:成功;1:失败;2:保留,不要使用这个值。

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

HEALTHCHECK NONE:如果基础镜像有健康检查指令,使用这行可以屏蔽掉其健康检查指令。

本文链接:https://cs.pynote.net/sf/docker/202110299/

-- EOF --

-- MORE --