Part8 Dockerfile:打造个性化容器

163 阅读10分钟

在容器化的世界里,Dockerfile 是构建容器镜像的核心工具。它是一份简单的文本文件,定义了如何从基础镜像开始,安装所需的依赖、复制文件、设置环境变量以及最终运行应用程序。理解 Dockerfile 的基本语法和结构,是掌握 Docker 容器化开发的关键步骤。

本文涉及的技术名词: FROMRUNCMDlayersDockerfile

在开始容器化应用程序的过程中,了解如何构建镜像是至关重要的。而构建镜像的基础正是 Dockerfile。它定义了容器的所有环境、依赖和配置,通过精确的步骤,帮助你创建符合项目需求的镜像。掌握 Dockerfile 的编写技巧,可以大大提高你的开发效率,并确保你的应用程序在任何环境中都能一致地运行。接下来,我们将从最基础的部分开始,详细解析 Dockerfile 的结构和用法。

构建镜像

$ docker image build [option] <path>
$ docker build [option] <path>
选项含义应用场景
-f, --file指定一个 Dockerfile在项目中使用多个 Dockerfile 时
-t, --tag为构建结果打标签为镜像结果添加易于理解的标识

这些选项能够帮助我们在构建镜像时更灵活地控制 Dockerfile 的使用,尤其是在需要管理多个镜像或不同环境的配置时,-f-t 是非常实用的工具。

检查镜像层

Docker 镜像是由多个层组成的,每一层代表镜像构建过程中的一个步骤。为了查看镜像的构建历史及其每个步骤的详细信息,可以使用 docker history 命令。这个命令展示了镜像的每一层是如何创建的,包括添加文件、安装依赖等操作。

使用方法

$ docker image history [option] <image>

我们以nginx为例:

$ docker history nginx

这个命令将显示镜像 nginx 的所有层,列出每一层的创建时间、大小和执行的命令。

应用场景

  • 调试构建步骤:查看镜像的每一层可以帮助我们理解构建过程,特别是在排查构建问题时非常有用。
  • 优化镜像大小:通过分析各层的大小,可以找出哪个步骤导致镜像过大,从而优化 Dockerfile。

通过 docker history,我们可以清楚地了解镜像是如何一步步构建起来的,并快速识别出潜在问题。

Dockerfile 的必要性与实用性

Dockerfile 是构建容器镜像的核心工具,对于自动化、优化和管理容器化应用至关重要。在开发和运维过程中,理解 Dockerfile 的作用与其带来的优势,能帮助我们更高效地管理容器和镜像。下面我们来看看容器和镜像的优势和短板:

1. 容器的短暂性

容器的设计初衷就是轻量和短暂。当一个容器终止时,所有在容器内所做的操作或修改都会消失。例如,在容器中安装了某些软件包,或者修改了系统配置,这些更改在容器停止后将不复存在。

这意味着,在每次启动容器时,如果希望容器有特定的配置和环境,需要有一个可靠的方式来确保这些配置始终如一。手动操作不仅效率低下,而且容易出错。Dockerfile 在这里的作用就是通过定义构建镜像的步骤,确保环境的可重复性和一致性。

2. 镜像的层结构

Docker 镜像由多个信息层(layers)组成,每一层代表镜像构建过程中的一个步骤。例如,一层可能用于安装软件包,另一层可能用于复制文件。通过分层结构,Docker 能够高效地管理和存储镜像,不同镜像之间可以共享相同的层,从而节省存储空间。

然而,这些镜像并不是“文件”或“程序”,而是由指令层层叠加的结果。这意味着每个镜像背后都有一套构建步骤。官方镜像通常只有最基本的层,保持其轻量化,确保它们能够快速启动和执行。

3. 官方镜像的轻量与局限

Docker Hub 上的官方镜像通常只有基本的操作系统或运行环境,例如一个最小化的 Ubuntu 镜像。虽然这让镜像保持了轻量化,但也限制了它们的功能。比如,官方的 Ubuntu 镜像可能不包含我们常用的工具,如 vicurl 等。每次启动容器后,如果需要这些工具,就得手动安装,耗时耗力,而且还容易遗忘或出现错误。

在这种情况下,官方镜像并不能满足我们的需求,需要一个带有自定义设置的镜像,比如预安装了常用工具、环境变量、配置文件等。

Dockerfile基础指令

Dockerfile 有许多指令可以使用,我们不需要一次性记住所有的指令。这里我们可以从一些最常见的指令入手,逐步了解它们的用途和作用。在本节中,我们通过对ubuntu官方基础镜像扩展的例子逐步编写一个 Dockerfile,并通过例子解释每个指令的具体作用。我们将创建一个简单的镜像,这个镜像可以显示时间,并使用 vim 编辑器显示行号。

常见的 Dockerfile 指令

指令作用
FROM指定基础镜像
RUN执行任意命令
COPY将主机上的文件添加到镜像中
CMD指定默认的执行指令

接下来,我将逐一解释这些指令的作用,并结合一个具体示例展示如何使用这些指令创建 Docker 镜像。

创建一个Dockerfile

在构建 Docker 镜像时,每个镜像都需要对应一个 Dockerfile。Dockerfile 是用来定义镜像构建过程的文件,包含了所有想要安装的依赖、文件和设置

Dockerfile 可以放置在主机上的任何目录中,Docker 对其位置没有特定要求。但为了保持项目的整洁和易于管理,最好为 Dockerfile 创建一个专门的目录。这些目录不需要任何特定的扩展名,只需选择一个合适的名称即可。

这里我们就创建一个名为 docker_project 的目录,并在该目录中放置 Dockerfile:

$ mkdir docker_project
$ cd my_docker_project
$ touch Dockerfile
FROM: 指定一个基础镜像

FROM 是用于指定基础镜像的指令,它定义了镜像的起点。每个 Dockerfile 都需要从一个基础镜像开始,这个基础镜像可以是操作系统、应用程序环境或自定义的镜像。

在 Dockerfile 中添加类似于以下的命令:

FROM ubuntu:20.04

这意味着基于 ubuntu:20.04 镜像来构建我们的新镜像。可以理解为,“在 Ubuntu 20.04 这个基础镜像的层之上,再添加更多自定义的层。”

在 Docker 中,镜像是由多个层构成的。每个指令(如 RUNCOPY 等)都会添加一层,FROM 是所有这些层的起点,它为接下来的操作提供了基础环境。在选择基础镜像时,可以根据项目的需求选择不同版本的镜像,比如更小的镜像以减小镜像体积,或者特定版本的镜像以确保环境的一致性。

RUN: 执行一个Linux命令

RUN 指令用于在构建过程中执行任意的 Linux 命令,并将执行结果保存为镜像中的一层。通过 RUN,可以安装软件包、配置环境或运行脚本,这些操作都会生成新的镜像层。

这里我们基于 ubuntu:20.04 的镜像中安装 vim 编辑器,可以在 Dockerfile 中添加如下命令:

FROM ubuntu:20.04
RUN apt-get update && apt-get install -y vim

小提示:RUN 指令中使用的包管理器取决于基础镜像。例如,ubuntu:20.04 使用 apt,而其他镜像(如 nginx:latest)可能使用不同的包管理器。可以通过运行镜像并进入 bash 来查看操作系统及其包管理工具。

COPY: 从宿主机添加一个文件到镜像

COPY 是用于将主机上的文件添加到镜像中的指令。通过 COPY,我们可以将配置文件、脚本或其他所需的文件从主机复制到容器内,使其成为镜像的一部分。

这里我们将.vimrc复制到镜像使每次启动容器时自动加载 vim 配置,显示行号:

  1. 在主机上创建 .vimrc 文件
    这个文件定义了 vim 编辑器的配置。在主机上创建 .vimrc 文件并设置显示行号的配置:
set number

2. 在 Dockerfile 中使用 COPY 命令
将主机上的 .vimrc 文件复制到镜像中的 /root/ 目录下,这样容器启动时会自动加载配置:

FROM ubuntu:20.04
RUN apt update
RUN apt install -y vim

# 将主机上的 .vimrc 文件复制到容器中的 /root 目录下
COPY .vimrc /root/.vimrc

通过这种方式,每次启动容器时,vim 都会根据配置文件自动显示行号,避免了每次启动容器时手动创建或设置 .vimrc 的麻烦。

CMD: Specify the default command

CMD 指令用于设置镜像的默认命令,这个命令将在容器启动时自动执行。如果没有为容器指定其他命令,CMD 中定义的命令将被执行。

这里我们希望容器启动时显示当前时间,并以特定格式输出,而不是启动默认的 bash 终端。我们在 Dockerfile 中通过 CMD 指令来实现这个目的:

FROM ubuntu:20.04

RUN apt update
RUN apt install -y vim

COPY .vimrc /root/.vimrc

CMD date +"%Y/%m/%d %H:%M:%S ( UTC )"
检查以上内容 查漏补缺
$ tree -a .

.
|-- .vimrc
`-- Dockerfile
FROM ubuntu:20.04

RUN apt update
RUN apt install -y vim

COPY .vimrc /root/.vimrc

CMD date +"%Y/%m/%d %H:%M:%S ( UTC )"
set number

构建镜像

上面我们根据自定义的需求编写完了dockerfile, 接下来我就开始构建镜像

$ docker image build [option] <path>

在构建 Docker 镜像时,建议使用 --tag 选项来为镜像添加一个标签。这样可以方便地识别和管理镜像,而不是依赖随机生成的 IMAGE ID

这里我们将镜像打上 custom-ubuntu:date 的标签:

$ docker build --tag custom-ubuntu:date .

上面的命令中:

  • --tag custom-ubuntu:date:为镜像指定标签 custom-ubuntu:date,方便之后引用。
  • . :指定当前目录作为 Dockerfile 和要使用文件的路径。

如果没有指定标签,Docker 将为镜像生成一个随机的 IMAGE ID,这在管理镜像时会变得不太方便。因此,建议始终在构建镜像时通过 --tag 选项为镜像添加有意义的标签。

执行上面命令后如果最后一行类似如下输出就意味着镜像构建成功了:

=> naming to docker.io/library/custom-ubuntu:date

使用docker image ls命令我们会找到已经构建好的镜像

为了验证新构建的镜像是否符合预期, 用如下命令启动一个容器:

$ docker container run --name custom-ubuntu --rm custom-ubuntu:date

当前时间已经正确的显示在终端上了, 符合预期!

接下来我们运行如下命令看看vim的配置是否可以达到预期

docker container run \
    --name ubuntu2  \
    --rm               \
    --interactive      \
    --tty              \
    custom-ubuntu:date     \
    vim

vi 显示从行号开始, 结果达到预期:

如何处理多个Dockerfile

在执行上看的构建镜像命令时, 你可能会疑惑为什么在构建镜像时没有为 Dockerfile 指定路径? 其实这是因为默认情况下 docker build 会在当前目录中寻找 Dockerfile,因此无需手动指定路径。如果你想从特定路径加载 Dockerfile,可以使用如下命令:

$ docker build -f ../Dockerfile .

这会告诉 Docker 使用位于上一级目录的 Dockerfile 来构建镜像。

在实际使用 Docker 进行开发时,通常会需要多个容器,例如 "前端应用容器", "后端应用容器"和"数据库容器"等。这意味着可能会有多个不同的 Dockerfile,每个文件对应一个镜像。因此,使用 COPY 指令时,通常会为每个镜像和 Dockerfile 创建单独的目录,这样可以更好地管理文件和依赖。

多个Dockerfile的我们后面单独拿出一个系列文章探讨, 这里我们先只展示将Dockerfile独立为一个文件目录的效果:

$ tree -a .

.
`-- docker
    `-- date
        |-- .vimrc
        `-- Dockerfile

这里我们将上面date相关的Dcokerfile以及.vimrc单独放在date目录下, 然后, 我们执行构建命令时需要指定--file<path>:

$ docker image build              \
    --tag custom-ubuntu:date          \
    --file docker-project/date/Dockerfile \
    docker-project/date

当指定了一个非默认路径的 Dockerfile 时,--file 选项是必需的,例如 ./Dockerfile。接下来,<path> 是用于 COPY 指令时的相对路径,用来指定要使用的文件位置。

例如,以下命令:

COPY .vimrc /root/.vimrc

会被解释为 .vimrc: 执行目录/<路径>/.vimrc,也就是从当前执行目录下的 <path> 位置复制 .vimrc 文件到镜像中的 /root/.vimrc 路径。

既然我们已经将 .vimrc 移动到 docker-project/date 目录中,那么我们需要在 COPY 中相应地指定正确的 <path>,如下:

$ tree -a .

$ docker image build [option] docker-project/date
`-- docker                    ^^^^^^^^^^^
    `-- date
        |-- .vimrc
        `-- Dockerfile  COPY (./)(docker-project/date/).vimrc /root/.vimrc
                                  ^^^^^^^^^^^

如果执行命令时还是想使用image build``., 那就要调整Dockerfile中的COPY:

$ tree -a .

$ docker image build [option] .
`-- docker                    ^
    `-- date
        |-- .vimrc
        `-- Dockerfile  COPY (./)(./)docker-project/date/.vimrc /root/.vimrc

查看层

通过image history命令, 我们可以查看镜像层信息

$ docker image history [option] <image>

我们来对比一下ubuntu:20.04``custom-ubuntu:date层的区别:

$ docker history ubuntu:20.04

IMAGE          CREATED       CREATED BY                                      SIZE      COMMENT
9df6d6105df2   3 weeks ago   /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B
<missing>      3 weeks ago   /bin/sh -c #(nop) ADD file:e7cff353f027ecf0a…   72.8MB
<missing>      3 weeks ago   /bin/sh -c #(nop)  LABEL org.opencontainers.…   0B
<missing>      3 weeks ago   /bin/sh -c #(nop)  LABEL org.opencontainers.…   0B
<missing>      3 weeks ago   /bin/sh -c #(nop)  ARG LAUNCHPAD_BUILD_ARCH     0B
<missing>      3 weeks ago   /bin/sh -c #(nop)  ARG RELEASE                  0B
$ docker history custom-ubuntu:date

IMAGE          CREATED             CREATED BY                                      SIZE      COMMENT
cfe71ef599ec   56 minutes ago      CMD ["/bin/sh" "-c" "date +"%Y/%m/%d %H:%M:…   0B        buildkit.dockerfile.v0
<missing>      56 minutes ago      COPY .vimrc /root/.vimrc # buildkit             11B       buildkit.dockerfile.v0
<missing>      56 minutes ago      RUN /bin/sh -c apt install -y vim # buildkit    68.2MB    buildkit.dockerfile.v0
<missing>      About an hour ago   RUN /bin/sh -c apt update # buildkit            53.4MB    buildkit.dockerfile.v0
<missing>      3 weeks ago         /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B
<missing>      3 weeks ago         /bin/sh -c #(nop) ADD file:e7cff353f027ecf0a…   72.8MB
<missing>      3 weeks ago         /bin/sh -c #(nop)  LABEL org.opencontainers.…   0B
<missing>      3 weeks ago         /bin/sh -c #(nop)  LABEL org.opencontainers.…   0B
<missing>      3 weeks ago         /bin/sh -c #(nop)  ARG LAUNCHPAD_BUILD_ARCH     0B
<missing>      3 weeks ago         /bin/sh -c #(nop)  ARG RELEASE                  0B

可以看到,RUNCOPYCMD 等指令在 Dockerfile 中执行时,会逐层堆叠。每一层构建完成后,都会生成一个唯一标识的 IMAGE ID。例如,完成的镜像会显示一个类似 cfe71ef599ec 的 IMAGE ID,这表明所有层已经成功构建并成为镜像的一部分。每一层的变化都会被保留在镜像历史中,确保可以跟踪到每个步骤的执行情况。

RUN 导致的层问题

RUN 指令会影响镜像的大小,因为每执行一个 RUN,都会生成一个新的镜像层。每个层都会保留当时的执行结果和文件。因此,过多的 RUN 指令会导致更多的镜像层,从而增加镜像的大小。

为了解决这个问题,开发者经常将多个命令合并在同一个 RUN 指令中,以减少生成的层:

$ RUN apt update && apt install -y vim

通过这种链式操作,多个命令可以在一个 RUN 中执行,这样可以减少镜像的层数,从而减小镜像的体积。然而,链式命令也有其缺点,尤其是当链条过长时:

  1. 命令难以调试:如果某个命令执行失败,整个 RUN 指令都会失败,但由于所有命令是在一个层中执行的,调试会变得困难。你可能不知道是哪一个命令导致了错误。
  2. 可读性差:当链式命令变得过长时,Dockerfile 的可读性会大大降低,增加了维护的复杂度。
  3. 构建失败风险更高:一旦其中一个命令执行失败,整个构建过程就会失败,并且之前的命令都需要重新执行。这在链条较长时尤其痛苦,因为它会浪费时间和资源。
平衡策略

为了解决这些问题,建议找到平衡点:

  • 链式合并重要步骤:可以将相关命令合并在一起,但不要将所有操作都放在同一个 RUN 中。例如,下载和清理操作可以放在一起:
RUN apt-get update && apt-get install -y vim curl && rm -rf /var/lib/apt/lists/*
  • 逐步执行关键步骤:对于可能会失败的关键步骤,建议分开执行,每个命令生成一个层。这样,如果某一步失败,可以很容易地调试和修复它:
RUN npm install
RUN npm run build

总结

  • FROM:指定基础镜像。
  • RUN:执行 Linux 命令,并将结果固定为镜像的一层。
  • COPY:将主机上的文件添加到镜像中。
  • CMD:指定容器启动时的默认命令。
  • <path> :构建镜像时,确保 COPY 使用正确的路径。
  • RUN 指令的决策点
  • 要考虑镜像大小、缓存优势等因素。
  • 避免链式命令过长,以减少构建和调试的难度。
  • FROM:指定的基础镜像层会作为 Dockerfile 中其他层的基础。