Dockerfile 是一个包含了用户所有构建命令的文本。通过 docker build 命令可以从 Dockerfile 生成镜像。

使用 Dockerfile 构建镜像具有以下特性:

  • Dockerfile 的每一行构建命令都会生成一个独立的镜像层,并且拥有唯一的 ID;
  • Dockerfile 的命令是完全透明的,通过查看 Dockerfile 的内容,就可以知道镜像是如何一步步构建的;
  • Dockerfile 是纯文本的,方便跟随代码一起存放在代码仓库并做版本管理;
  • 使用 Dockerfile 构建镜像无须考虑构建环境,基于相同 Dockerfile 无论在哪里运行,构建结果都一致。

常用指令

指令简介
FROMFROM 后面跟镜像名称,代表要基于哪个基础镜像构建容器
LABELLABEL 用于为镜像添加元数据
RUNRUN 后面跟一个具体的命令,类似于 Linux 命令行执行命令
ADD拷贝本机文件或者远程文件到镜像内
COPY拷贝本机文件到镜像内
USER指定容器启动的用户
ENTRYPOINT容器的启动命令
CMDCMD 为 ENTRYPOINT 指令提供默认参数,也可以单独使用 CMD 指定容器启动参数
ENV指定容器运行时的环境变量,格式为 key=value
ARG定义外部变量,构建镜像时可以使用 build-arg = 的格式传递参数用于构建
EXPOSE指定容器监听的端口,格式为 [port]/tcp 或者 [port]/udp
WORKDIR为 Dockerfile 中跟在其后的所有 RUN、CMD、ENTRYPOINT、COPY 和 ADD 指令设置工作目录

命令详解

以下面这份 Dockerfile 文件为例,解释一下命令的详细含义。

FROM alpine
LABEL maintainer="nigelpoulton@hotmail.com"
RUN apk add --update nodejs nodejs-npm
COPY . /src
WORKDIR /src
RUN npm install
EXPOSE 8080
ENTRYPOINT ["node", "./app.js"]

每个 Dockerfile 文件除注释外第一行都是 FROM 指令。FROM 指令指定的镜像,会作为当前镜像的一个基础镜像层,当前应用的剩余内容会作为新增镜像层添加到基础镜像层之上。

Dockerfile 中通过标签(LABLE)方式指定了当前镜像的维护者为 "nigelpoulton@hotmail. com"。每个标签其实是一个键值对(Key-Value),在一个镜像当中可以通过增加标签的方式来为镜像添加自定义元数据。

RUN apk add --update nodejs nodejs-npm 指令使用 alpine 的 apk 包管理器将 nodejs 和 nodejs-npm 安装到当前镜像之中。RUN 指令会在 FROM 指定的 alpine 基础镜像之上,新建一个镜像层来存储这些安装内容。

COPY . /src 指令将应用相关文件从构建上下文复制到了当前镜像中,并且新建一个镜像层来存储。

下一步,Dockerfile 通过 WORKDIR 指令,为 Dockerfile 中尚未执行的指令设置工作目录。该目录与镜像相关,并且会作为元数据记录到镜像配置中,但不会创建新的镜像层。

RUN npm install 指令会根据 package.json 中的配置信息,使用 npm 来安装当前应用的相关依赖包。npm 命令会在前文设置的工作目录中执行,并且在镜像中新建镜像层来保存相应的依赖文件。

因为当前应用需要通过 TCP 端口 8080 对外提供一个 Web 服务,所以在 Dockerfile 中通过 EXPOSE 8080 指令来完成相应端口的设置。这个配置信息会作为镜像的元数据被保存下来,并不会产生新的镜像层。

最后,通过 ENTRYPOINT 指令来指定当前镜像的入口程序。ENTRYPOINT 指定的配置信息也是通过镜像元数据的形式保存下来,而不是新增镜像层。

最终构建的镜像结构如下:

image.png

在上面的例子当中,新增镜像层的指令包括 FROM、RUN 以及 COPY,而新增元数据的指令包括 EXPOSE、WORKDIR、ENV 以及 ENTERPOINT。关于如何区分命令是否会新建镜像层,一个基本的原则是,如果指令的作用是向镜像中增添新的文件或者程序,那么这条指令就会新建镜像层;如果只是告诉 Docker 如何完成构建或者如何运行应用程序,那么就只会增加镜像的元数据。

可以通过 docker image history 来查看在构建镜像的过程中都执行了哪些指令。

观察 docker image build 命令具体的输出内容,可以了解到镜像构建的过程。基本的构建过程是,运行临时容器 > 在该容器中运行 Dockerfile 中的指令 > 将指令运行结果保存为一个新的镜像层 > 删除临时容器。

CMD 和 ENTRYPOINT

CMD 和 ENTRYPOINT 指令都是容器运行的命令入口,这两个指令使用中有很多相似的地方,但是也有一些区别。

shell 格式和 exec 格式

我们可用两种方式指定 CMD 和 ENTRYPOINT 要运行的命令:shell 格式和 exec 格式,二者在使用上有细微的区别。

  • shell 格式:CMD/ENTRYPOINT command param ,这种格式是基于 shell 实现的, Docker 会以 /bin/sh -c command 的方式执行命令
  • exec 格式:CMD/ENTRYPOINT ["command" , "param"],这种格式是使用 Linux 的exec实现的,当指令执行时,会直接调用 [command],不会被 shell 解析。这种也是 Docker 推荐的使用格式。
CMD

CMD 指令允许用户指定容器的默认执行的命令。如果 docker run 指定了其他命令,CMD 指定的默认命令将被忽略。如果 Dockerfile 中有多个 CMD 指令,只有最后一个 CMD 有效。

CMD ["param1", "param2"] 要与 exec 格式的 ENTRYPOINT 指令配合使用,其用途是为 ENTRYPOINT 设置默认的参数。

ENTRYPOINT

ENTRYPOINT 指令可让容器以应用程序或者服务的形式运行。ENTRYPOINT 看上去与 CMD 很像,它们都可以指定要执行的命令及其参数。不同的地方在于即使运行 docker run 时指定了其他命令,ENTRYPOINT 也不会被忽略,一定会被执行。需要在启动 Docker 容器时使用 --entrypoint 参数才能被覆盖。

ENTRYPOINT 的 exec 格式用于设置要执行的命令及其参数,同时可通过 CMD 提供额外的参数。此时ENTRYPOINT 中的参数始终会被使用,而 CMD 的额外参数可以在容器启动时动态替换掉。ENTRYPOINT 的 shell 格式会忽略任何 CMD 或 docker run 提供的参数。

如果你希望你的镜像足够灵活,推荐使用 CMD 指令。如果你的镜像只执行单一的具体程序,并且不希望用户在执行 docker run 时覆盖默认程序,建议使用 ENTRYPOINT。

ADD 和 COPY

ADD 和 COPY 指令功能类似,都是从外部往容器内添加文件。但是 COPY 指令只支持基本的文件和文件夹拷贝功能,ADD 则支持更多文件来源类型,比如自动提取 tar 包,并且可以支持源文件为 URL 格式

比较推荐的是使用 COPY 指令,因为 COPY 指令更加透明,仅支持本地文件向容器拷贝,而且使用 COPY 指令可以更好地利用构建缓存,有效减小镜像体积。想要使用 ADD 向容器中添加 URL 文件时,请尽量考虑使用其他方式替代,如 RUN wget...。

最佳实践

保持镜像最小化

容器的核心是应用,只要基础镜像能够满足应用的运行环境即可。使用 FROM 指令引用官方基础镜像是一个很好的习惯,选择一个相对较小的镜像文件通常也能避免一些潜在的问题。

应该避免安装无用的软件包,在构建 Linux 镜像时,若使用的是 APT 包管理器,则应该在执行 apt-get install 命令时增加 no-install-recommends 参数。这能够确保 APT 仅安装核心依赖(Depends 中定义)包,而不是推荐和建议的包。

同时也应该尽量最小化镜像的层数。每一个RUN指令会新增一个镜像层。因此,通过使用 && 连接多个命令以及使用反斜杠(\)换行的方法,将多个命令包含在一个 RUN 指令中,通常来说是一种值得提倡的方式。

使用构建缓存

Docker 的构建过程利用了缓存机制。docker image build 命令会从顶层开始解析 Dockerfile 中的指令并逐行执行。而对每一条指令,Docker 都会检查缓存中是否已经有与该指令对应的镜像层。如果有,即为缓存命中(Cache Hit),并且会使用这个镜像层;如果没有,则是缓存未命中(Cache Miss),Docker 会基于该指令构建新的镜像层。

一旦有指令在缓存中未命中(没有该指令对应的镜像层),则后续的整个构建过程将不再使用缓存。在编写 Dockerfile 时须特别注意这一点,尽量将易于发生变化的指令置于 Dockerfile 文件的后方执行。

通过对 docker image build 命令加入 --nocache=true 参数可以强制忽略对缓存的使用。

还有一点也很重要,那就是 COPY 和 ADD 指令会检查复制到镜像中的内容自上一次构建之后是否发生了变化。例如,有可能 Dockerfile 中的 COPY . /src 指令没有发生变化,但是被复制的目录中的内容已经发生变化了。为了应对这一问题,Docker 会计算每一个被复制文件的 checksum 值,并与缓存镜像层中同一文件的 checksum 进行对比。如果不匹配,那么就认为缓存无效并构建新的镜像层。


与昊
222 声望634 粉丝

IT民工,主要从事web方向,喜欢研究技术和投资之道