[chat-20230424]docker分段构建

发布时间 2023-04-24 13:35:38作者: coreylin

为什么 docker 分段构建,能减小镜像大小

docker 镜像组成

一个Docker镜像包含以下几部分组成:

应用程序和依赖:Docker镜像中可以包含一个或多个应用程序及其依赖库。这些文件通常保存在以只读层(镜像层)的形式存在的文件系统中。

环境变量和配置:在运行该镜像的容器时,可以为其指定一组环境变量和配置项,来进行启动设置。这些设置通常在Dockerfile中定义,并以容器配置文件(如config.v2.json)的形式保存在镜像中。

元数据信息:Docker镜像包含了一些元数据信息,例如镜像名称、版本号、作者、制作时间、所使用的存储驱动类型等等。这些信息保存在JSON格式的Manifest文件中,后缀名可以是“json”、“yml”或“toml”。

存储引擎和驱动: Docker支持多种存储引擎和驱动,通常用于管理和维护镜像和容器的底层文件系统。常见的存储引擎包括AUFS、OverlayFS、btrfs、ZFS等。

网络设置:Docker镜像包括一套网络设置,比如IP地址、网关信息、DNS配置等等。这些设置在运行容器时进行指定,并以容器配置文件的形式保存在镜像中。

Dockerfile指令:Docker镜像是通过一个或多个Dockerfile来构建的。每个Dockerfile包含一系列指令,用于描述如何构建以及打包需要的文件系统和依赖库。

综上所述,Docker镜像是由多个部分组成的,包括应用程序、依赖库、环境变量、配置项、元数据信息、存储驱动、网络设置和Dockerfile指令等等。这些部分不仅使得镜像具有高度可移植性和可重复构建性,同时也是实现Docker高效操作和部署的重要因素。

只读层与读写层

一个Docker镜像通常由多个只读层和一个可写层(也称为读写层或容器层)组成。使用docker inspect命令可以查看这些层及其属性,以区分只读层和读写层。

通常情况下,只读层的ID会出现在{{.RootFS.Layers}}中,例如:

$ docker inspect --format='{{.RootFS.Layers}}' <镜像名称/ID>

返回类似 ["sha256:123", "sha256:456", ...] 的结果,其中每个 sha256 后面跟着的就是不同只读层的 ID。

而读写层则是通过命令输出中的Mounts字段来进行标识。该字段将显示文件系统挂载点的详细信息,包括源路径、目标路径和挂载选项。例如:

$ docker inspect --format='{{json .Mounts}}' <容器名称/ID>

上述命令将返回包含所有挂载信息的JSON对象,其中就包括了读写层的详细信息。

需要注意的是,Docker镜像本身是只读的,因此不能更改已经打包进去的只读层。如果需要更改文件系统内容,则应该运行一个基于镜像的容器,并在容器中添加或修改所需的文件,从而生成新的读写层。这样,在容器停止时,读写层也随之删除,并且容器的更改将不会影响原始镜像。

docker 打包过程

Docker打包成的最终镜像除了包含以只读层(镜像层)和读写层(容器运行时修改的层)组成的文件系统外,还包含了以下信息:

  1. 元数据:元数据记录了关于镜像的一些基本信息,比如镜像名称、版本号、制作者信息、创建时间、镜像大小等等。这些信息保存在镜像的JSON文件中。

  2. 镜像配置文件:镜像配置文件也是以JSON格式保存在镜像中。它包含了启动容器时所需的所有信息,如环境变量、命令列表、默认入口点、资源限制等。

  3. 存储驱动:存储驱动是指Docker使用的后端存储技术,用来管理和运行容器的文件系统和镜像。Docker支持多个存储驱动,如AUFS、OverlayFS、device mapper等。

  4. 网络设置:网络设置包括容器的IP地址、网关、DNS信息等。

  5. 其他元素:根据Dockerfile中定义的特定操作,镜像中还可能包含自己的应用程序、二进制依赖包等。

总的来说,Docker的最终镜像是一个完整的、可重复部署的软件包,它不仅包含了应用程序、依赖库等资源,还包含了一些元数据和配置信息,以及Docker运行时所需的驱动和网络设置。

分段构建

Docker分段构建可以通过将Dockerfile中的指令分解为多个独立的步骤,每个步骤都生成一个中间镜像层,从而减小最终镜像的大小。这是因为每个中间镜像层只包含特定步骤产生的文件和状态,而不包含整个系统的完整状态。因此,在进行下一步操作之前,可以删除不再需要的文件和状态,从而使每个中间镜像层更轻巧、精简。这种方法还可以加速构建过程,因为只有修改了Dockerfile中的某个步骤时才需要重新构建该步骤之后的所有步骤,而不是整个镜像。

假设我们有一个应用程序需要依赖Python环境和一些第三方库。如果我们使用单个的Dockerfile来构建这个应用程序的镜像,整个过程可能会像这样:

FROM python:3.9-slim-buster

RUN apt-get update && apt-get install -y \
  build-essential \
  python-dev \
  libpq-dev \
  && rm -rf /var/lib/apt/lists/*

WORKDIR /app

COPY requirements.txt /app/
RUN pip install --no-cache-dir -r requirements.txt

COPY . /app

CMD ["python", "app.py"]

在这个Dockerfile中,我们将所有步骤都放在一个RUN指令下面,并且没有任何清理步骤。这意味着生成的镜像将包含中间文件和状态(如缓存、编译输出、未清理的缓存等),这些文件和状态可能不再需要,而且会占用宝贵的存储空间。

现在,假设我们将这个Dockerfile拆分为多个独立的步骤,如下所示:

# 第一阶段:安装必要的软件
FROM python:3.9-slim-buster AS base

RUN apt-get update && apt-get install -y \
  build-essential \
  python-dev \
  libpq-dev \
  && rm -rf /var/lib/apt/lists/*

# 第二阶段:安装Python依赖库
FROM base AS dependencies

WORKDIR /app

RUN pip install --no-cache-dir pip -U
COPY requirements.txt /app/
RUN pip install --no-cache-dir -r requirements.txt

# 第三阶段:复制应用程序代码
FROM dependencies AS source

WORKDIR /app

COPY . /app

# 第四阶段:生产环境镜像
FROM source AS release

CMD ["python", "app.py"]

在这个新的Dockerfile中,我们将构建过程分解为了四个独立的步骤。每个阶段都以前一个阶段构建的镜像为基础,并且执行特定的任务。在第一、二和三个阶段中,我们使用了"AS"关键字来为当前中间镜像层命名,并在后续指令中使用该名称来引用它们。在第四个阶段中,我们只复制了应用程序代码,没有将多余的构建工具或包含中间状态的文件打包到最终生成的镜像中。

通过这种方式,我们可以在不增加额外成本的情况下减小镜像大小,提高构建效率,同时更灵活地管理和维护我们的Dockerfile。

为什么 docker 分段构建,能减小镜像大小

分段构建产生的中间镜像层能够不包括缓存,是因为每个阶段的Docker指令会生成一个新的镜像层。在这个新的镜像层中,只包含了当前阶段所需的文件和状态,而不包括之前的阶段产生的临时文件和中间状态。这种方式下,随着构建流程的持续进行,旧的镜像层和其中的临时文件会被逐渐丢弃和回收,因此最终构建出的镜像不会包含多余的内容。

直接构建产生的最终镜像则不同,它是基于单个的Dockerfile构建的,并不是通过多个阶段构建的。在这种情况下,每个Docker指令都在同一个临时容器中执行,构建过程中的所有中间状态和临时文件都会以读写层的形式存在于同一个目录结构中,这就是所谓的Docker缓存。如果没有在Dockerfile中清理不再需要的文件或状态,这些中间文件也会被打入到最终镜像中。

因此,使用分段构建可以有效避免Docker缓存造成的问题,从而减小镜像大小并提高构建效率。