@[toc]
🌟 前言
Docker 镜像是 Docker 容器的基石,容器是镜像的运行实例,有了镜像才能启动容器。
Docker 镜像是一个只读的模板,一个独立的文件系统,包括运行一个容器所需的数据,可以用来创建容器。
1. base镜像
base(基础) 镜像是指完全从零开始构建的镜像, 它不会依赖其他镜像,甚至会成为被依赖的镜像,其他镜像以它为基础进行扩展。
通常 base 镜像都是 Linux 的系统镜像, 如 Ubuntu、CentOS、Debian 等。
下面通过 Docker 拉取一个 base 镜像并查看, 这里以 CentOS 为例, 示例代码如下:
从以上示例中可以看出,一个 CentOS 镜像大小只有 202MB,但在安装系统时,一个 CentOS 大概有几 GB,这与操作系统有关。
先观察 Linux 原本的操作系统结构,如图所示👇
Kernel 是内核空间。bootfs 文件系统在 Linux 启动时加载。rootfs 是包含操作命令的文件系统。
base 镜像的创建过程中,Kernel、 bootfs 与 rootfs 都会加载,然后 bootfs 文件系统 (包括 Kernel) 被卸载掉,镜像只保留 rootfs 文件系统,供用户进行操作。bootfs 与 Kernel 将与宿主机共享。
另外,为了增加 Docker 的灵活性,base 镜像提供的都是最小安装的 Linux 系统。
Linux 系统不同的发行版之间最大的区别就是 rootfs 的不同,例如,Ubuntu 系统的应用程序管理器是 apt,而 CentOS 是 yum。
由此可见,只要提供不同的 rootfs 文件系统就可以同时支持多种操作系统,如图所示👇
从上图中可以看到,两个不同的 Linux 发行版提供了各自的 rootfs 文件系统,而它们共用的是底层宿主机的 Kernel。
假设宿主机的系统是 Ubuntu 16.04,Kernel 版本是 4.4.0,无论 base 镜像原本的发行版 Kernel 版本如何,在这台宿主机上都是 4.4.0。
下面通过示例来验证,示例代码如下:
从上述示例中可以看出,base 镜像与宿主机的 Kernel 版本都是 3.10。
base 镜像的 Kernel 是与宿主机共享的,其版本与宿主机一致,并且不能进行修改。
2. 镜像的本质
Docker 镜像是一个只读的文件系统,由一层一层的文件系统组成,每一层仅镜像的本质包含前一层的差异部分,这种层级文件系统被称为 UnionFS。
大多数 Docker 镜像都在 base 镜像的基础上进行创建,每进行一次新的创建就会在镜像上构建一个新的 UnionFS。
查看 ubuntu:15.04 镜像的层级结构,示例代码如下:
通常,对 Docker 的操作命令都是以 “docker” 开头。pull 是下载镜像的命令,在英文中是 “拉” 的意思,所以下载镜像又叫作 拉取镜像。
以上示例中,第 5 行到第 8 行是每一层 UnionFS 的 ID 号,第 9 行是整个镜像的 ID 号,这个 ID 号可以用来操控镜像。
然后,查看镜像,示例代码如下:
在以上示例中,不仅可以看到先前下载的 Ubuntu15.04 镜像,还可以看到其他镜像,说明 docker images
是查看本地所有镜像的命令。而查看到的信息中,除了镜像名称,还有版本号、镜像 ID 号、创建时间以及镜像大小。
接着,通过命令查看镜像的构建过程,示例代码如下:
这里使用 “history” 与镜像 ID 号组合的命令查看镜像构建过程,所显示的信息包括镜像 ID 号、创建时间、由什么命令创建以及镜像大小。
从以上示例中的信息可以看出,ubuntu:15.04 镜像由四个只读层 (Read Layer) 构建而成,每一层都是由一条命令构成的,最终得到 ID 号为 dlb55fd07600 的镜像,但以用户的视角只能看到最上层。
当用户将这个镜像放在容器中运行时,四层之上会创建出一个可读可写层(Read-Write Layer),用户对 Docker 的操作都通过可读可写层进行。如果用户修改了一个已存在的文件,那该文件将会从可读可写层下的只读层复制到可读可写层,该文件的只读版本仍然存在,只是已经被可读可写层中该文件的副本所隐藏。
可读可写层又叫作容器层,只读层又叫作镜像层,容器层之下均为镜像层,层级结构如图所示👇
镜像的这种分层机制最大的一个好处就是:共享资源。
例如,有很多个镜像都基于一个基础镜像构建而来,那么在本地的仓库中就只需要保存一份基础镜像,所有需要此基础镜像的容器都可以共享它,而且镜像的每一层都可以被共享,从而节省磁盘空间。
因为有了分层机制,本地保存的基础镜像都是只读的文件系统,不用担心对容器的操作会对镜像有什么影响。
为了将零星的数据整合起来,人们提出了镜像层 (Image Layer) 这个概念,如图所示👇
下图所示为一个镜像层,我们能够发现,一个层并不仅仅包含文件系统的改变,它还能包含其他重要的信息。
元数据 (Metadata) 就是关于这个层的额外信息,包括 Docker 运行时的信息与父镜像层的信息,并且只读层与可读可写层都包含元数据,如图所示👇
除此之外,每一层还有一个指向父镜像层的指针。如果没有这个指针,说明它处于最底层,是一个基础镜像,如图所示👇
3. 查找本地镜像
Docker 本地镜像通常是储存在服务器上的,下面验证本地镜像的储存路径,示例代码如下:
从以上示例中可以看到,Docker 本地镜像储存路径是 /var/Iib/Docker
。
在本地查看镜像时,通常使用 docker images
命令,示例代码如下:
从以上示例中可以看到,结果显示中有多项镜像信息,下面对信息进行解释。
🍇 REPOSITORY
镜像仓库,即一些关联镜像的集合。
例如,Ubuntu 的每个镜像对应着不同的版本。与 Docker Registry 不同,镜像仓库提供 Docker 镜像的存储服务。
即 Docker Registry 中有很多镜像仓库,镜像仓库中有很多镜像 (相互独立)。
🍇 TAG
镜像的标签,常用来区分不同的版本,默认标签为 latest。
🍇 IMAGE ID
镜像的ID号,镜像的唯一标识,常用于操作镜像 (默认值只列出前 12 位)。
🍇 CREATED
镜像创建的时间。
🍇 SIZE
镜像的大小。
🍇 参数用法
在 docker images
命令后加上不同的参数就形成了不同的查询方式,导致不同的查询结果。
下面介绍各参数的含义以及用法。
-
-a
表示显示所有本地镜像,默认不显示中间层镜像,这是工作中经常使用到的参数,用来从本地镜像中寻找符合生产条件的镜像。
示例代码如下: -
-q
表示只显示本地所有镜像 ID 号。
示例代码如下: - -no-trunc
表示使用不截断的模式显示,并显示完整的镜像 ID 号。
示例代码如下:
4. 构建镜像
Docker 的官方镜像库 Docker Hub 发布了成千上万的公共镜像供全球用户使用。用户可以直接拉取(下载)所需要的镜像,提高了工作效率。但是在很多工作环境中,一旦对镜像有特殊需求,就需要我们手动去构建镜像。
本文章将会介绍基于 docker commit
命令与 Dockerfile 两种方式来构建自己的 Docker 镜像。
🍑 使用 docker commit 命令构建镜像
使用 docker commit
命令将容器的可读可写层转换为一个只读层,这样就把一个容器转换成了一个不可变的镜像,如图所示👇
下面我们给一个 Centos 的镜像安装一个 Vim 服务,设置开机启动,并将其构建成一个新的镜像,以免每次启动容器都要再次安装 Vim。
首先启动一个 Centos 的容器,示例代码如下:
从以上示例中可以看到,容器启动之后,主机名发生了改变,说明用户直接进入了容器,再进行操作就是对容器的操作。
然后,在容器中安装 Vim,示例代码如下:
安装完成之后,退出容器,示例代码如下:
使用 exit 命令退出容器之后, 该容器将默认关闭。
下面使用 docker commit
命令在 CentOS 镜像的基础上创建新的镜像,示例代码如下:
在命令中需要用镜像 ID 号来指定基础镜像,并不需要将 ID 号都输入进去,只要输入几个字符使 ID 号与其他镜像不冲突即可。
此时可以看到刚刚构建的新镜像,代码如下:
从以上示例中可以看到, 新镜像的大小是 326MB,而此前的 CentOS 镜像只有 202MB, 这是因为在安装 Vim 时还安装了许多依赖包。
然后, 查看镜像中是否已经自动安装了 Vim, 示例代码如下:
从以上示例中可以看到,新镜像已经包含了 Vim。
这种构建新镜像的方式在工作中并不常见,原因如下。
(1)效率低下,如果要给 Ubuntu 镜像也添加一个Vim,需要将上述全部过程重复一遍。
(2)不透明,用户使用时不知道镜像是如何构建的,难以对镜像做出正确的判断。
🍑 使用 Dockerfile 构建镜像
镜像可以基于 Dockerfile 构建。Dockerfile 是一个描述文件,包含若干条命令,每条命令都会为基础文件系统创建新的层次结构,这正好弥补了 docker commit
构建镜像效率低下的缺点。
Dockerfile 定义容器内部环境中发生的事情。网络接口和磁盘驱动器等资源的访问在此环境内虚拟化,与系统的其余部分隔离。
Dockerfile 主要使用 docker build
命令,根据 Dockerfile 文件中的指令,执行若干次 docker commit
命令构建镜像,每次执行 docker commit
命令时都会生成一个新的层,因此许多新的层会被创建,如图所示👇
🍑 Dockerfile常用命令
下面介绍 Dockerfile 中常用的命令,完整说明见官方文档。
-
FROM
指定源镜像,必须是已经存在的镜像,必须是Dockerfile中第一条非注释的命令,因为其后的所有指令都使用该镜像。
-
MAINTAINER
指定作者信息。
-
RUN
在当前容器中运行指定的命令。
-
EXPOSE
指定运行容器时要使用的端口。可以使用多个EXPOSE命令。
-
CMD
指定容器启动时运行的命令,Dockerfile 可以出现多个 CMD 指令,但只有最后一个生效。CMD 可以被启动容器时添加的命令覆盖。
-
ENTRYPOINT
CMD 或容器启动时添加的命令会被当做参数传递给 ENTRYPOINT。
-
COPY
文件或目录复制到当前容器中。
-
ADD
将文件或者目录复制到当前容器中,源文件如果是归档(压缩)文件,则会被自动解压到目标位置。
-
VOLUME
为容器添加容器卷,可以存在于一个或多个目录,用来提供共享存储。该命令会在容器数据卷部分详细介绍。
-
WORKDIR
在容器内设置工作目录。
-
ENV
设置环境变量。
- USER
指定容器以什么用户身份运行,默认是 root。
🍑 运行一个Dockerfile
下面演示使用 Docker file 创建 centos/vim
,示例代码如下:
这里在宿主机的 root 目录下创建了一个 Dockerfile 文件。
接着,向 Docker file 文件中添加内容, 示例代码如下:
添加完成之后,保存并退出。
有了 Dockerfile 文件之后即可创建新的镜像,示例代码如下:
通过 docker build
命令执行 Dockerfile 文件,-t 用来指定新镜像名为 centos/vim-Dockerfile
,命令行末尾的 .
表示 Dockerfile 文件在当前目录,Docker 默认从指定的目录寻找 Dockerfile 文件,也可以使用 -f 参数指定 Dockerfile 文件的位置。
构建完成之后,查看镜像是否构建成功,示例代码如下:
从以上示例中可以看到,新镜像已经构建成功。
使用 Dockerfile 构建镜像基本可以分为以下五步。
(1) 选择一个基础镜像,运行一个临时容器。
(2) 执行一条命令,对容器做修改。
(3) 执行类似 docker commit
的操作,生成一个新的镜像。
(4) 删除临时容器,再基于刚刚构建好的新镜像运行一个临时容器。
(5) 重复 (2) (3) (4) 步,直到执行完 Dockerfile 中的所有指令。
centos/vim-Dockerfile
由 CentOS 基础镜像和 RUN yum -y install vim
构成,现在两个镜像都包含了 ID 号为 lel148e4cc2c 的只读层,如图所示👇
以上结论可以使用 docker history
命令验证,docker history
命令专门用来查看镜像的结构,示例代码如下:
这里可以看到 CentOS 镜像中确实包含了 ID 号为 1e1148e4cc2c 的只读层。
接着再查看新镜像 centos/vim-Dockerfile
的结构,示例代码如下:
从以上示例中可以看到,两个镜像都含有一个相同的只读层,并且这个只读层是共享的。
Docker 构建镜像时有缓存机制,如果构建镜像层时该镜像层已经存在,就直接使用,无须重新构建。
下面为先前的 Dockerfile 文件添加一点内容,安装一个 ntp 服务,重新构建一个新的镜像,示例代码如下:
这里多加了一条安装 ntp 服务的命令。
添加完成后,开始创建镜像,示例代码如下:
在示例的第 6 行代码中可以看到,Docker 没有重新安装 Vim,而是直接使用了先前安装过的缓存。
Dockerfile 文件是从上至下依次执行的,上层依赖于下层。无论什么时候,只要某一层发生变化,其上面所有层的缓存都会失效。
改变先前的 Dockerfile 文件中两条 RUN 命令的上下顺序,观察 Docker 还会不会使用缓存机制,示例代码如下:
将 Dockerfile 中两条 RUN 命令的顺序互换之后,开始创建镜像,示例代码如下:
由以上验证可知,将两条 RUN 命令交换顺序导致镜像层次发生改变,Docker 会重建镜像层。由此可见 Docker 的镜像层级结构特性:只有下面的层次内容、顺序完全一致才会使用缓存机制。
如果在构建镜像时不想使用缓存,可以在 docker build
命令中添加 --no-cache
参数,否则默认使用缓存。
除了在使用 Dockerfile 构建镜像时有缓存机制,在从仓库拉取镜像时也会有缓存机制,即已经拉取到本地的镜像层可以被多个镜像共同使用,可以说是一次拉取多次使用,前提是下层镜像完全相同。
通常使用 Dockerfile 构建镜像时,如果由于某些原因镜像构建失败,我们能够得到前一个指令成功执行构建出的镜像,继而可以运行这个镜像查找指令失败的原因,这对调试 Dockerfile 有极大的帮助。
从 Docker Hub 拉取的 CentOS 镜像是最小化的,其中没有 vim 命令。下面测试错误构建 Docker 镜像的结果,示例代码如下:
将 Dockerfile 中任意一条 RUN 命令改为错误的,再开始创建镜像,示例代码如下:
在示例中,由于第三步报错,镜像没有创建成功。但也生成了一个新镜像,这个镜像是第二步操作构建的,通常可以通过这个新镜像排查错误,示例代码如下:
Docker 容器技术中,编写 Dockerfile 文件是非常重要的部分,下面总结编写 Dockerfile 文件的一些小技巧,相信可以帮助大家更好地使用 Docker 与 Dockerfile。
-
(1)容器中只运行单个应用。
从技术角度讲,在一个容器中可以实现整个 LNMP (Linux+Nginx+MySQL+PHP) 架构。但这样做有很大的弊端。首先,镜像构建的时间会非常长,每次修改都要重新构建;
其次,镜像文件会非常大,大大降低容器的灵活性。 -
(2)将多个 RUN 指令合并成一个。
众所周知,Docker 镜像是分层的,Dockerfile 中的每一条指令都会创建一个新的镜像层,镜像层是只读的。
Docker镜像层类似于洋葱,想要更改内层,需要将外层全部撕掉。 -
(3)基础镜像的标签尽量不要使用 latest。
当镜像的标签没有指定时,默认使用 latest 标签。
当镜像更新时,latest 标签会指向不同的镜像,可能会对服务产生影响。 -
(4)执行 RUN 命令后删除多余文件。
假设执行了更新 yum 源的命令,会自动下载解压一些软件包,但是在运行容器的时候不需要这些包。最好将它们删除,因为这些软件包会使镜像 SIZE 变大。
-
(5)合理调整 COPY 与 RUN 的顺序。
将变化少的部分放在 Dockerfile 文件的前面,充分利用镜像缓存机制。
- (6)选择合适的基础镜像。
最好选择满足环境需要而且体积小巧的镜像,比如 Alpine 版本的 node 镜像。
Alpine 是一个极小化的 Linux 发行版,只有 5.5MB,非常适合作为基础镜像。