Docker生命周期

1、Docker三阶段

1)Dockerfile

本质

Dockerfile是用于构建Docker镜像的脚本文件,其中存放了镜像构建的一系列有先后顺序的步骤(指令)

内容

如何构建镜像的一系列步骤:

  FROM:基于哪个基础镜像

  RUN:安装哪些依赖

  COPY:复制哪些代码

  CMD:启动命令

大小

Dockerfile文本文件由于只是指令的文本描述(纯文本文件),不包含任何实际的依赖和文件内容,大小只有KB级别。可以类比为C语言的源代码文件(.cpp,大小几KB)。

生命周期

由用户手动创建,独立于Docker引擎存在,不占用容器空间,仅作为普通文本文件存在于宿主机

作用

作为docker build的输入,定义镜像的构建规则

2)Docker build

本质

Dockerfile还要经过Build(构建),构建出一个“可运行文件系统集合”,这些文件被永久固化在镜像的只读层中,是容器运行的“基础文件集”。

Docker会按照Dockerfile中的指令(如FROM、COPY、RUN)逐步执行,每一步生成一个镜像层Layer,最终叠加生成一个完整的镜像。

内容

  ①包含了基础镜像的

    编译产物(如RUN gcc app.c -o app生成的可执行文件)

    通过COPY/ADD复制的代码(如index.html)

    RUN安装的依赖(如Nginx二进制文件)等, 

  ②记录镜像ID、标签、构建历史、默认启动命令(CMD/ENTRYPOINT)、工作目录(WORKDIR)等信息

大小

体积一般为MB~GB级别(取决于应用类型)。可以类比cpp文件编译后产生的.obj文件(.obj,大小KB~MB级别)。

生命周期

  镜像通过build构建完成后,其只读层、元数据就不会被修改(除非重新构建),可被多次用于创建容器(docker run)。镜像中的文件是“静态的”,不随容器运行而变化(容器本身运行过程中对于文件的修改,仅会被记录在容器自身的可写层,不影响镜像,因此重启后这些修改就会消失,恢复最初的状态)。

  build构建完成后,除非通过docker rmi手动删除,否则生成的文件系统会持久存在于宿主机中(存储在Docker数据目录中,如/var/lib/docker/)。

特点

  静态只读,无法修改。除非通过修改dockerfile再build之后重新构建,将其作为模板后可重新创建出初始状态一模一样的容器。

用途

  作为docker run的输入,提供容器运行所需的基础文件、配置。

3)docker run

docker run基于镜像(即2中build后产生的文件系统集合+镜像属性(如ID、标签等)创建并启动容器,(如果说build是编译,那么run就是运行可执行程序,真正生成进程)容器运行中会产生如下文件:

  ①容器的可写层文件

    容器启动时,Docker会在镜像只读层上添加一个可写层,容器内的文件修改(如新建文件、修改配置)都会被记录到这里(不影响镜像本身,所以容器重启后一切做的临时修改都会被抹除);

  ②挂载卷(Volumes)中的文件

    若通过了docker run -v挂载了宿主机目录、命名卷,容器内对于挂载路径上的文件操作(如写入日志、数据库文件)会直接存储在宿主机,实现持久化修改

  ③容器元数据

    记录容器ID、名称、端口映射、资源限制等信息,用于Docker引擎管理容器生命周期。

生命周期

  与容器绑定,容器删除、重启后,可写层的文件会被清理,但是挂载卷中的文件会保留在宿主机上。

2、镜像、容器

镜像的本质是docker build之后生成的文件系统集合+镜像属性元数据构成的一个“只读模板”

容器的本质是docker run镜像启动后生成的可运行实例(进程)

3、关于docker build生成的镜像

1)文件系统

  docker build生成的只读文件系统层(存储在/var/lib/docker/overlay2等目录,如果是K8S会是其他的目录,如/var/lib/containerd),这些层叠加起来形成一个完整的可运行的文件系统:

  a)包含了OS核心文件(如/bin/lib)、应用依赖(如Nginx二进制文件)、代码(如app.py)等运行所需的静态文件;

  b)这个文件系统时docker run启动容器的“基础模板”,容器启动时会直接挂载这些只读层,并在其上添加一个可写层(用于运行时修改)

 

2)镜像属性元数据

 docker build除了生成文件系统外,还会生成元数据(如镜像ID、CMD/ENTRYPOINT启动命令WORKDIR工作目录),这些数据告诉docker run如何激活文件系统。

这个元数据位于/var/lib/docker/image/overlay2/imagedb/content/sha256/目录下,是个以该镜像的SHA256值为名的文件,每次build之后都会在该目录下新生成一个文件。

 

docker run正是基于以上包含了完整文件系统运行规则(元数据)镜像,创建出可读写的容器实例——本质上是对镜像文件系统的动态使用(复用只读层+隔离可写层)。

 

可运行文件系统集合

上述1、2中的分层文件系统、元数据共同构成了一个可运行的文件系统集合,这就是Docker镜像的本质

这个集合不仅包含了应用运行的所需的所有文件(如代码、依赖、二进制程序),还包含了让Docker引擎能够正确启动、管理容器的元数据,是容器运行的“基础模板”。

完整性

1中所说的分层文件系统,包含了最小化可运行环境的全部文件,在overlay2目录下的diff/文件中,包含了从基础镜像应用代码的完整文件集:

  • 基础层:OS核心文件(如/bin、/lib下的系统库);
  • 中间层:通过RUN安装的依赖(如Nginx、Python解释器);
  • 顶层:通过COPY/ADD复制的应用代码(如app.py、配置文件)

这些文件(层)组合起来,形成了一个类似精简操作系统的文件系统,足以支撑应用运行。

可操作性

2中所说的元数据(config.json),定义了如何“激活”这个文件系统:

  • 启动命令(CMD、ENTRYPOINT):告诉Docker启动容器时需要执行哪个程序(如nginx -g )
  • 工作目录(WORKDIR):定义程序运行的默认路径;
  • 环境变量(ENV):提供程序运行所需的配置参数

这些元数据让静态的文件系统集合能够被docker引擎激活为动态的运行环境。

隔离性

  • 多个容器共享同一个镜像的只读层;
  • 每个容器对文件系统的修改仅保存在自身的可写层(该可写层在容器启动时会为每个容器分一个,互不影响),不影响镜像和其他容器。

3)镜像标签

build时通常会用-t参数给镜像打上标签,例如docker build -t app:latest .,这里就给最新构建的镜像打上了标签app:latest。

这个标签存在于文件repositories.json中,位置为:

  /var/lib/docker/image/overlay2/repositories.json

打开repositories.json后,可以看到类似以下的结构

{
  "Repositories": {
    "app": {  // 镜像仓库名(如 "app")
      "latest": "sha256:a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b"  // 标签 "latest" 对应的镜像 ID
    },
    "ubuntu": {  // 基础镜像仓库
      "22.04": "sha256:f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5"
    }
  }
}

该文件在Docker中,全局唯一,且每次build都会修改其值(如果是新标签,就在其中新增一个值),因此就算你习惯不好,连续build了很多次,打的标签都是app:latest;Docker也会正确识别出该镜像是哪个,因为就算标签一样,但是repositories.json中这个标签对应的镜像的SHA-256的值都是不一样的(除非你没改dockerfile,重复build,此时镜像本身没变化,它的SHA-256值也不会变化)。

repositories.json

该文件的核心作用是记录当前Docker环境中所有镜像仓库的标签(tag)镜像ID的映射关系(如nginx:latest、my-app:v1.0等),它有如下特性:

  • 对于单Docker守护进程管理的环境,该文件是全局唯一的;
  • 无论有多少个镜像、多少个仓库(如nginx、ubuntu、自定义应用仓库),所有标签的映射关系都会集中存储在该文件中(而非按照仓库拆分为多个);
  • 每次执行docker tag、docker rmi、docker build -t等操作,Docker都会直接修改这一个文件,确保所有标签映射的一致性、唯一性

4)关于路径

在docker学习的过程中,经常会看到如下路径(或类似路径):

/var/lib/docker/image/overlay2/repositories.json

解析:

  • /var/lib/docker:Docker默认的数据根目录(可通过docker info | grep "Docker Root Dir"确认实际路径)
  • image/overlay2:overlay2存储驱动的元数据目录(若使用其他驱动,如devicemapper,则路径会变为image/devicemapper

WORKDIR

容器内部的工作目录

WORKDIR指令可以用来设置容器内部的默认工作目录,不涉及宿主机的文件系统。

若Dockerfile中包含了WORKDIR /u01/app/rules,那么相当于把后续容器中的工作目录都置为了/u01/app/rules,如果用了指令ls,则会列出/u01/app/rules中的文件信息。

实际位置

容器内的/u01/app/rules目录,物理上存储在宿主机的Docker数据目录的对应层中:

/var/lib/docker/overlay2/<层ID>/diff/u01/app/rules

不过这是Docker联合文件系统的内部映射,对于容器内的进程而言,它看到的就是逻辑上的/u01/app/rules路径,完全感知不到宿主机内的/var/lib/docker。

4、文件系统集合中的layer

之前一直提到这个概念,即layer。Dockerfile中每多一行可生成层的指令(如RUN、COPY、ADD等),就会生成一个新层,这些层在真实的宿主机中,确实是一个个独立的目录,且通过每层的元数据info.json,找出该层的父层(即上一条指令生成的层)。层与层的核心就体现在存储的文件内容层间关联关系上,具体如下:

1)层在真实文件系统中的物理形态

在主流的overlay2存储驱动下,每层都对应宿主机数据目录中的一个独立子目录

以containerd为例,路径为/var/lib/containerd/overlayfs/snapshots/,之后dockerfile建立而成的一层层,就在这个snapshots目录下,作为它的子目录:

/snapshots/
├── 1/                # 层1(基础层,如 FROM 指令对应的基础镜像层)
│   ├── diff/         # 该层新增/修改的文件(真实目录结构)
│   │   ├── bin/
│   │   ├── etc/
│   │   └── ...
│   └── info.json     # 层元数据(记录父层ID、创建时间等)
├── 2/                # 层2(基于层1的新层,如 RUN 指令生成)
│   ├── diff/         # 该层新增的文件(如安装的依赖)
│   │   ├── usr/
│   │   └── ...
│   └── info.json     # 元数据中记录父层为1
└── 3/                # 层3(基于层2的新层,如 COPY 指令生成)
    ├── diff/         # 该层新增的应用代码
    │   ├── app/
    │   └── ...
    └── info.json     # 元数据中记录父层为2

从上表可以看出,不同指令构成的上下层之间,并不是通过嵌套子目录的方式形成结构的,而是作为同一级目录中的子目录。目录间的关系,通过各层的info.json来关联,这个元数据中记录了该层的父层(本质上是生成该层的指令的上一条指令所形成的层)。

虽然物理上是并列目录,但是Docker/containerd会通过联合文件系统(如overlay2),根据info.json中记录的依赖关系,将这些并列的层逻辑上堆叠起来,形成一个统一的文件系统视图:

  • 先生成的层的内容会被后生成的层继承,如有同名文件,则后生成的层会覆盖掉上一层;
  • 容器中看到的 /(即根目录),就是这些并列层被合并后的结果,在容器使用过程中完全感知不到物理上的并列存储。

注意:以上1、2、3只是举例,实际中名称本身并不代表顺序,仅做唯一标识。

 2)层与层之间的核心区别

①diff/目录的不同

注意:diff/和diff(是否带斜杠)本质上是同一个目录,斜杠仅用于明确表示“这是一个目录路径”,而不改变目录本身的身份和内容。

每层的diff/目录仅包含该层新增、修改的文件/目录,这是层与层之间最直观的区别:

例如:

  RUN apt install nginx:该层的diff/会包含usr/sbin/nginx、etc/nginx/等nginx相关文件;

  COPY app.py /app/:该层的diff/仅包含app/app.py文件,不会包含前一层的nginx文件。

这种“只记录差异”的特性,使层与层之间的内容天然隔离,避免重复存储。

②元数据文件info.json记录的依赖关系不同

每层都有一个元数据文件info.json,其中会记录父层ID,形成清晰的依赖链:

  • 基础层(FROM层)没有父层;
  • 后续每层都以上一层为父层,例如层2的父层为1,层3的父层为2(如果多次build,且没有清除之前的层,可能出现层4的父层为2这种情况)。

注:层1的parent为空。

③对最终文件系统的贡献不同

若多层通过联合挂载合并,则后生成的层会覆盖前一层的同名文件

  • 若层2的diff/etc/nginx.conf与层1的diff/etc/nginx.conf同名,则合并后显示层2的文件;
  • 若层3删除了层2的某个文件(通过RUN rm),则层3的diff/目录中会生成一个“删除标记”,合并后该文件会被隐藏

3)总结

层是独立目录 + 差异内容 + 依赖关系的组合:

  独立目录:每多一层就会在diff下多一个子目录,代表该层,不同层在物理结构上平级,不存在嵌套关系。

  差异内容:diff目录中仅记录该层的差异文件和元数据文件

  依赖关系:层通过info.json中记录的父层ID形成依赖链,最终被合并为一个完整的文件系统。

4)疑问

①我首次build后,形成了1、2、3三层,如果我对第三层进行了修改,是会生成一个3.1层吗?

修改了第三层对应的dockerfile指令,如将COPY app.pu /app/改为COPY app_v2.py /app/,重新build后,会:

  • Docker发现层3指令被修改,因此不会复用之前的层3;
  • 基于层2新建一个层4(层4的父层为层2),层4的diff目录包含了修改后的文件app_v2.py
  • 新镜像的依赖链变为:1→2→4

对于原来的层3:

  • 层3仍会保留(除非手动清理),仍以独立目录形式存在于宿主机的存储路径中;
  • 但是新镜像不会和层3再产生关联了。

这涉及到我们之前说的:Docker镜像的层一旦创建就是只读的任何修改都必须通过创建新层来实现。

②虽然可以通过每一层的info.json来上溯它的父层,但是Docker是如何从全局层面知道哪个是最终层呢?如果不知道又该怎么知道如何从1正向构建到最后一层呢?

先看之前说过的——每个层建立完毕后,都会在该层的元数据文件中(如info.json或类似结构),通过parent(父层ID)字段记录该层是基于哪个层构建的:

  • 层1的parent为空;
  • 层2的parent为1;
  • 层3的parent为2;
  • 当层3被修改并生成层4后,层4的parent仍为2。

镜像构建完成后,会生成一份镜像配置文件config.json,其中就有一个关键配置rootfs.layers,它记录了该镜像的所有层的哈希号,使Docker可以从头到尾找到正确的镜像。

因此Docker并非依靠之前说的info.json来找到全部层的。info.json是层级别的元数据文件,而config.json是镜像的配置文件,它俩的区别在于:

  • 位置

    • info.json位于/var/lib/docker/overlay2/<层ID>目录下
    • config.json为/var/lib/docker/image/overlay2/imagedb/content/sha256/<镜像ID>下(标红的位置即为二者路径分道扬镳之处)。
  • 内容

    • info.json记录的是该层的信息:id、parent、created(创建时间)、overlay(层类型);
    • config.json记录镜像的全局配置:
      • rootfs.layers数组类型,按顺序列出该镜像所有层的SHA-256哈希(就是这个配置项,使得Docker可以从头到尾找到关联的所有层)
      • 容器的启动配置(config,如CMD、WORKDIR、ENV);
      • 镜像创建时间(created)
      • parent:该镜像的基础镜像ID。
  • 例子

  假设一个镜像由层1、2、3构成,那么在各层info.json中,就会出现如下写法:

    • 层1的info.json,会有parent:""(空,代表无父层)
    • 层2的info.json,会有parent:"层1的ID;
    • 层3的info.json,会有parent:"层2的ID"

  而在镜像的config.json中,则会有:

"rootfs":{
    "type":"layers",
    "layers":[
        "层1的sha256值",
        "层2的sha256值",
        "层3的sha256值"
    ]
}

查找与验证

rootfs.layers定义镜像的层顺序,Docker只需按照该数组顺序读取层,并根据层的info.json中的parent验证层之间的依赖关系,共同保证镜像文件系统的正确构建。

实际过程为:

  1. Docker读取config.json中的rootfs.layers数组,得到层ID的有序列表;
  2. 逐个读取每层的info.json,并验证parent字段是否与前一层的ID匹配:
    • 层2的parent必须为1的ID;
    • 层3的parent必须为2的ID。
  3. 若所有层的parent都验证通过,则按照rootfs.layers的顺序堆叠这些层,形成完整的文件系统。

③build重新构建镜像,会对镜像所在目录、镜像各层所在的目录有啥影响?

首先明确一个前提:

镜像和镜像的各层是保存在不同的目录下的:

  • info.json位于/var/lib/docker/overlay2/<层ID>目录下
  • config.json为/var/lib/docker/image/overlay2/imagedb/content/sha256/<镜像ID>下(标红的位置即为二者路径分道扬镳之处)。

这俩文件分别代表层元数据、镜像元数据所在的目录。

每次build,都会:

镜像

  • /var/lib/docker/image/overlay2/imagedb/content/sha256/目录下生成一个新的文件文件、非目录),文件名是新镜像完整的SHA-256哈希值(即镜像ID),该文件包含了新镜像的config.json配置信息(包含了rootfs.layers等核心元数据)。
  • 如果未修改Dockerfile,重复build,由于前后两次镜像的SHA-256哈希值没发生变化,因此Docker会复用现有层和镜像配置,不会重复生成新文件。
  • 如果修改了Dockerfile,即使构建时的标签相同,

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

posted @ 2025-09-03 15:47  ShineLe  阅读(20)  评论(0)    收藏  举报