Dockerfile 文件介绍

概述

Dockerfile 是一个文本文件,其中包含若干指令,用以层级构建 docker image。

为方便介绍 Dockerfile 有关内容,需要简单说明 imagecontainer 的底层原理。

image 实际是一个层级只读文件,其结构大致如图所示:

container 可理解为镜像之上添加一个可读写层。容器之中所执行的任何操作 (例如,添加内容、修改 image 内容) 都会保存至该可读写层。

此时我们看一下 docker commit 操作,基于此操作构建得到的镜像等价于运行此容器所使用的镜像 + 该可读写层。这基本是 Dockerfile 构建镜像的原理,即以基本镜像为基础,生成容器并运行它,执行相关操作,docker commit 得到中间镜像,随后以此中间镜像为基础,再次执行类似步骤,最终得到目标镜像。

Dockerfile

此节介绍 dockerfile 之中所涉及的各种内容。

需要注意两点:

  1. Dockerfile 生成器将自上而下、逐行执行 Dockerfile 文件中的指令
  2. Dockerfile 中每条构建指令的执行结果都会成为目标镜像中的一层

解析器指令

解析器指令为 Dockerfile 的可选内容,它会影响 Dockerfile 文件中后续指令的执行方式。

其具体格式为 # directive=value,且特定的 directive 仅能出现一次。Dockerfile 支持的解析器指令具体如下:

  • syntax –> # syntax [remote image reference]

    syntax 指令用于指定构建当前 Dockerfile 文件所使用的 Dockerfile 生成器位置。

    例如:# syntax=docker/dockerfile:1.0.0,指定 Dockerfile 生成器位置为远端仓库的 docker/dockerfile:1.0.0。

  • escape –> # escape \

    escape 指令用于指定 Dockerfile 文件中的转义字符,默认转义字符为 \

    例如:# escape |,指定转义字符为 |

如果注释、空行、生成指令均已按序执行完成,则后续出现的解析器指令将被自动认定为注释。因此,为保证解析器指令正确执行,最好将其置于 Dockerfile 文件顶部。

解析器指令的执行结果不会成为目标镜像的一层,也不会出现在构建输出结果之中。

注释

除解析器指令外,凡是以 # 开头的行均为注释行。

执行 Dockerfile 前,会自动移除其中注释。因此,当分行书写一条指令时,其中穿插注释并不会影响指令执行效果。

.dockerignore 文件

该文件类似于 .gitignore 文件,用于排除使用 Dockerfile 构建镜像过程中所涉及的特定文件或文件夹。

构建指令

构建指令为 Dockerfile 的主体内容,它定义了镜像的具体构建过程。

构建指令一般格式为 INSTRUCTION arguments ,其中 INSTRUCTION 指明具体指令,arguments 为所涉参数。Dockerfile 支持的构建指令具体如下:

  • FROM –> FROM [--platform=<platform>] <image>[:<tag>] [AS <name>]

    FROM 指令用于指明目标镜像所使用的基础镜像。

    例如:FROM --platform=linux/amd64 centos:lastest AS CS,以 centos:lastest 为基础镜像,并将此构建阶段命名为 CS,同时指定构建平台为 linux/amd64 架构 (默认为本机所处的构建平台)。

    除解析器指令、注释、ARG 指令外,FROM 指令应当为 Dockerfile 文件自上而下的第一条指令,其标志 build stage 的开始。

  • RUN –> RUN <command> <param1> / RUN ['executable','param1']

    RUN 指令用于生成目标镜像的一层,该层为基于当前最新中间镜像得到的容器的可读写层。

    例如:RUN 'echo $HOME',基于当前最新中间容器得到的镜像中执行 echo $HOME shell 命令,并以可读写层作为目标镜像的一层。

  • ADD –> ADD [--chown=<user>:<group>] <src>... <dest>

    ADD 指令用于将 src 所指代的文件、文件夹或远程文件 URL 添加至当前最新中间镜像的文件系统之中。

    例如:ADD test.txt /usr/,将上下文中的 test.txt 文件添加至文件系统的 /usr/ 处。

  • COPY –> COPY [--chown=<user>:<group>] <src>... <dest>

    COPY 指令用于将 src 所指代的文件或文件夹添加至当前最新中间镜像的文件系统之中。

    COPY 指令存在可选参数 --from=<name>,其可用于在 multi-stage 之中重置当前指令所使用的 stage context

  • ENTRYPOINT –> ENTRYPOINT <command> <param1> / ENTRYPOINT ['executable','param1']

    ENTRYPOINT 指令用于配置容器启动时自动执行的命令

    它具有两种使用方式:shell 形式 (ENTRYPOINT <command> <param1>) 和 exec 形式 (ENTRYPOINT ['executable','param1']),前者会首先调用 shell 窗口,随后在其中执行命令,后者则直接执行命令。

    如果 Dockerfile 中存在多条 ENTRYPONT 指令,则最后一个 ENTRYPOINT 指令会生效。另外,如果不希望默认执行 ENTRYPOINT 所指代的命令,可在运行容器时使用参数 --entrypoint 重新设置。

    例如:ENTRYPOINT ['/bin/echo','hello'],容器启动后,它会自动执行命令 /bin/echo 'hello'

  • CMD –> CMD <command> <param1> / CMD ['executable','param1'] / CMD ['param1']

    CMD 指令用于配置容器启动时自动执行的默认命令

    它具有三种用法,前两种用法与 ENTRYPOINT 相同,在此仅说明第三种用法,即 CMD ['param1']。它需要与 ENTRYPOINT 指令联合使用,并以 param1 作为 ENTRYPOINT 指令所执行命令的参数。

    如果 Dockerfile 中存在多条 CMD 指令,则最后一个 CMD 指令会生效。另外,如果用户运行待执行容器时指定相关参数,则其会覆盖 CMD 指令。

    鉴于 ENTRYPOINT 与 CMD 指令十分相似,在此联合二者举例。

    假定 Dockerfile 含有如下内容:

    1
    2
    ENTRYPOINT echo 
    CMD ['hello']

    当运行容器时,如果具体命令为 docker run centos,则容器启动后自动执行 echo "hello";如果具体命令为 docker run centos "world",则容器启动后自动执行 echo "world";如果具体命令为 docker run --entrypoint='touch' centos,则容器启动后自动执行 touch "hello"

  • EXPOSE –> EXPOSE <port>

    EXPOSE 指令用于指明目标镜像向外发布的端口,它类似于一个介于发布镜像者与运行容器者之间的端口文档说明。

    例如:EXPOSE 80/udp,向外暴露面向 UDP 的 80 端口。

  • VOLUME –> VOLUME [mountpoint]

    VOLUME 指令用于创建一个挂载点。

    当运行容器时,如果没有显式挂载此挂载点,则其会默认创建匿名卷挂载此挂载点。

    例如:VOLUME '/lib/bin'

  • USER –> USER <user>:[group]

    USER 指令用于设置 UID 和 GID 以供容器运行时使用。

  • WORKDIR –> WORKDIR <path>

    WORKDIR 指令用于设置容器运行的工作目录。

    例如:WORKDIR /bin,设置容器的 /bin 目录为工作目录。

  • LABEL –> LABEL <key>=<value>

    LABEL 指令用于为目标镜像添加元数据。

    例如:LABEL version="1.0"

  • MAINTAINER –> MAINTAINER <name>

    MAINTAINER 指令用于指明维护者信息,也可使用 LABEL maintainer=<name> 达到同样效果。

    例如:MAINTAINER "Stivin"

  • ENV –> ENV <key>=<value>

    ENV 指令用于设置运行于容器时的环境变量 (将持久化于容器之内),其可应用于 Dockerfile 文件中的后续指令中。

    例如:ENV 'PATH'='/bin/lib',设置环境变量 PATH/bin/lib

  • ARG –> ARG <name>[=<value>]

    ARG 指令用于设置 Dockerfile 文件中所使用到的变量 (不会存在于容器之内)。所定义的变量从其定义位置开始生效,在此位置之前引用将得到空字符串。

  • ONBUILD –> ONBUILD xxx

    ONBUILD 指令用于为目标镜像添加触发器 (本质就是若干指令)。如果目标镜像作为其他 Dockerfile 文件中的基础镜像,则 Dockerfile 生成器执行完 FROM xxx 后,这些触发器会自动执行。

    例如:ONBUILD ADD . /app/src

  • STOPSIGNAL –> STOPSIGNAL <signal>

    STOPSIGNAL 指令用于设置用以容器退出的信号。

    例如:STOPSIGNAL SIGKILL

  • HEALTHCHECK –> HEALTHCHECK [option] CMD command / HEALTHCHECK none

    HEALTHCHECK 指令用于设置执行容器健康检查时应当做的操作。

    HEALTHCHECK 指令的两种形式中,HEALTHCHECK [option] CMD command 用于设置具体执行哪些操作,HEALTHCHECK none 表示禁止继承自基础镜像的任何健康检查操作。

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

  • SHELL –> SHELL ['exectuable','paramters]

    SHELL 指令用于重写默认 shell 。

multi-stage

利用上面所学的各种指令,我们可以很容易地编写一个构建特定目标镜像的 Dockerfile 文件。但是,编写 Dockerfile 文件的难点在于如何保证目标镜像大小尽可能地小。这其中存在众多技巧,在此介绍一种常见技巧。

考虑一种常见场景:项目开发过程中,往往存在两个环境——应用开发环境和应用部署环境。当项目开发完成后,就需要将其放到应用部署环境之中。

就目前所知,我们应该会构建两个 Dockerfile 文件:第一个 Dockerfile 文件用于构建开发环境,并拷贝项目代码至镜像中,运行得到二进制程序后,将其拷贝至本地;第二个 Dockerfile 文件用于构建部署环境,并将先前得到的二进制程序放置其中。

这种过程比较复杂,而且会产生中间镜像 (即根据第一个 Dockerfile 所产生的目标镜像),十分繁琐。

如果借助于 multi-stage,我们只需构建一个 Dockerfile 文件,即可达到同样的效果。

在该种情况下,Dockerfile 文件中会存在两个 stage (每个 stageFROM 指令标识开始,以下一个 FROM 指令标识结束),第一个 stage 用于构建开发环境,并拷贝项目代码至镜像中,运行得到二进制程序即可,第二个 stage 构建部署环境,并借助于 COPY 指令的 --from 参数将第一个 stage 中的二进制程序放置其中。

使用 Dockerfile 生成器构建 multi-stage 相关的 Dockerfile 文件时,其会保存各 stage 的运行结果以供其他 stage 使用,但是仅最后一个 stage 的运行结果会保存于目标镜像之中。

Docker build

命令 Docker build 用于根据 Dockerfile 文件构建目标镜像。

1
2
3
4
5
6
7
8
9
10
11
12
Usage:  docker build [OPTIONS] PATH | URL | -

Build an image from a Dockerfile

Options:
--add-host list Add a custom host-to-IP mapping (host:ip)
--build-arg list Set build-time variables
--cache-from strings Images to consider as cache sources
-f, --file string Name of the Dockerfile (Default is 'PATH/Dockerfile')
--no-cache Do not use cache when building the image
-t, --tag list Name and optionally a tag in the 'name:tag' format
...

当在 shell 窗口输入此命令后,客户端会将 Dockerfile 以及上下文环境 (指代运行此命令的当前目录的全部内容,如果其中文件符合 .dockerignore,则其会被忽略) 发送至守护进程,守护进程验证 Dockerfile 语法无误后,使用 Dockerfile 生成器依次执行 Dockerfile 文件中指令,以逐步构建目标镜像。

目标镜像构建过程往往是比较缓慢的。为解决此问题,Docker 借助于缓存机制以加快构建过程。

该缓存机制可简单分解为如下三条规则 (这里暂不考虑解析器指令):

  1. 如果指令文本未曾发生变动,则可直接重用先前构建的缓存结果。
  2. 对于 COPY、ADD 指令而言,如果所涉文件和指令文本均未曾发生变动,则可直接重用先前构建的缓存结果。
  3. 如果当前指令构建的层无法使用缓存结果,则其后续指令构建的层亦不能使用缓存结果。

因为存在缓存机制,编写高效 Dockerfile 的技巧之一便是:将不变部分尽可能写于 Dockerfile 文件前部。

举例而言,假定初始 Dockerfile 如下:

1
2
3
4
FROM python:3.7-slim-buster
COPY . .
RUN pip install --quiet -r requirements.txt
ENTRYPOINT ["python", "server.py"]

使用命令 docker build 构建目标镜像,可得到如下运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Sending build context to Docker daemon   5.12kB
Step 1/4 : FROM python:3.7-slim-buster
---> f96c28b7013f
Step 2/4 : COPY . .
---> eff791eb839d
Step 3/4 : RUN pip install --quiet -r requirements.txt
---> Running in 591f97f47b6e
Removing intermediate container 591f97f47b6e
---> 02c7cf5a3d9a
Step 4/4 : ENTRYPOINT ["python", "server.py"]
---> Running in e3cf483c3381
Removing intermediate container e3cf483c3381
---> 598b0340cc90
Successfully built 598b0340cc90
Successfully tagged example1:latest

不修改 Dockerfile,再次执行命令 docker build,可得到如下运行结果 (step2-4 均使用缓存结果):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Sending build context to Docker daemon   5.12kB
Step 1/4 : FROM python:3.7-slim-buster
---> f96c28b7013f
Step 2/4 : COPY . .
---> Using cache
---> eff791eb839d
Step 3/4 : RUN pip install --quiet -r requirements.txt
---> Using cache
---> 02c7cf5a3d9a
Step 4/4 : ENTRYPOINT ["python", "server.py"]
---> Using cache
---> 598b0340cc90
Successfully built 598b0340cc90
Successfully tagged example1:latest