更新中…

《每天5分钟玩转Docker容器技术》
《每天5分钟玩转Docker容器技术》

第 3 章 Docker 镜像

3.1 镜像的内部结构

3.1.1 最小的镜像 hello-world

docker pull hello-world
docker images
docker run hello-world
Hello from Docker!
This message shows that your installation appears to be working correctly.

hello-worldDockerfile 内容如下:

FROM scratch
COPY hello /
CMD ["/hello"]
  1. FROM scratch:此镜像是从白手起家,从 0 开始构建
  2. COPY hello /:将文件 hello 复制到镜像的根目录
  3. CMD ["/hello"]:容器启动时,执行 /hello

hello-world 虽然是一个完整的镜像,但它并没有什么实际用途。通常来说,我们希望镜像能提供一个基本的操作系统环境,用户可以根据需要安装和配置软件。这样的镜像我们称作 base 镜像

3.1.2 base 镜像

base 镜像有两层含义:

  • 不依赖其他镜像,从 scratch 构建
  • 其他镜像可在其基础上进行扩展

所以,能称作 base 镜像的通常都是各种 Linux 发行版的 Docker 镜像,比如 Ubuntu, Debian, CentOS 等。

使用 docker pull centos命令下载 centos 镜像并查看其镜像信息,发现大小仅为 200 MB,为什么会这么小?

Linux 操作系统内核空间用户空间组成,如下图所示:

Linux 的内核空间与用户空间
Linux 的内核空间与用户空间
rootfs

内核空间Kernel,Linux 刚启动时会加载 bootfs 文件系统,之后 bootfs 会被卸载掉。

用户空间的文件系统是 rootfs,包含我们熟悉的 /dev, /proc, /bin 等目录。

对于 base 镜像而言,底层直接用 Host 的 kernel,自己只需提供 rootfs。

而对于一个精简的 OS,rootfs 可以很小,只需要包括最基本的命令、工具和程序库就可以了。

base 镜像提供最小安装的 Linux 发行版

CentOS 镜像的 Dockerfile 文件内容如下:

FROM scratch
ADD centos-7-docker.tar.xz /
CMD ["/bin/bash"]

第二行 ADD 指令添加到镜像的 tar 包就是 CentOS 7 的 rootfs。在制作镜像时,这个 tar 包会自动解压到 / 目录下,生成 /dev, /porc, /bin 等目录。

支持运行多种 Linux OS

不同 Linux 发行版的区别主要就是 rootfs

比如 Ubuntu 14.04 使用 upstart 管理服务,apt 管理软件包;而 CentOS 7 使用 systemdyum。这些都是用户空间上的区别,Linux kernel 差别不大。

所以 Docker 可以同时支持多种 Linux 镜像,模拟出多种操作系统环境。

Debian 和 BusyBox 容器共用 Host Kernel
Debian 和 BusyBox 容器共用 Host Kernel

上图 Debian 和 BusyBox(一种嵌入式 Linux)上层提供各自的 rootfs,底层共用 Docker Host 的 kernel

需要说明的是:

  1. base 镜像只是用户空间与发行版一致,Kernel 版本与发行版是不同的
  2. 容器只能使用 Host 的 Kernel,并且不能修改。

3.1.3 镜像的分层结构

Docker 支持通过扩展现有镜像,创建新的镜像

实际上,Docker Hub 中 99% 的镜像都是通过在 base 镜像中安装和配置需要的软件构建出来的。例如我们现在构建一个新的镜像,Dockerfile 如下:

FROM debian
RUN apt-get install emacs
RUN apt-get install apache2
CMD ["/bin/bash"]

构建过程如下图所示:

构建过程示意
构建过程示意

可以看到,新镜像是从 base 镜像一层一层叠加生成的。每安装一个软件,就在现有镜像的基础上增加一层。

Docker 镜像采用这种分层结构最大的好处就是 共享资源

比如:有多个镜像都从相同的 base 镜像构建而来,那么 Docker Host 只需在磁盘上保存一份 base 镜像;同时内存中也只需加载一份 base 镜像,就可以为所有容器服务了。而且镜像的每一层都可以被共享,我们将在后面更深入地讨论这个特性。

如果多个容器共享一份基础镜像,当某个容器修改了基础镜像的内容,比如 /etc 目录下的文件时,修改会被限制在单个容器内,这就是容器的 Copy-on-Write 特性。

可写的容器层

当容器启动时,一个新的可写层会被加载到镜像的顶部。这一层通常被称作容器层,容器层之下的都叫镜像层

可写的容器层会被加载到镜像顶部
可写的容器层会被加载到镜像顶部

所有对容器的改动——无论是添加、删除还是修改文件,都只会发生在容器层中

并且,只有容器层是可写的,容器层下面的所有镜像层都是只读的

镜像层数量可能会很多,所有镜像层会联合在一起组成一个统一的文件系统。在容器层中,用户看到的是一个叠加之后的文件系统。

  • 添加文件:在容器中创建文件时,新文件被添加到容器层中
  • 读取文件:在容器中读取某个文件时,Docker 会从上往下依次在各镜像层中查找此文件。一旦找到,打开并读入内存。
  • 修改文件:在容器中修改已存在的文件时,Docker 会从上往下依次在各镜像层中查找此文件。一旦找到,立即将其复制到容器层,然后修改之
  • 删除文件:在容器中删除文件时,Docker 也是从上往下依次在镜像层中查找此文件。找到后,会在容器层中记录下此删除操作
Copy-on-Write

只有当需要修改时才复制一份数据,这种特性被称作 Copy-on-Write。可见,容器层保存的是镜像变化的部分,不会对镜像本身进行任何修改。

这样就解释了之前的问题:容器层记录对镜像的修改,所有镜像层都是只读的,不会被容器修改。所以镜像可以被多个容器共享

3.2 构建镜像

对于 Docker 用户来说,最好的情况是不需要自己创建镜像。使用现成镜像的好处除了省去自己做镜像的工作量外,更重要的是可以利用前人的经验。

当然,某些情况下我们也不得不自己构建镜像,比如:

  • 找不到现成的镜像,比如自己开发的应用程序
  • 需要在镜像中加入特定的功能,比如官方镜像几乎都不提供 ssh

Docker 提供了两种构建镜像的方法:

  1. docker commit 命令
  2. Dockerfile 构建文件

3.2.1 docker commit

docker commit 命令是创建新镜像最直观的方法,其过程包含三个步骤:

  • 运行容器
  • 修改容器
  • 将容器保存为新的镜像

(1) 运行容器

docker run -it ubuntu

(2) 安装 vi

vim
bash: vim: command not found
apt-get install vim
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following additional packages will be installed:
  file libexpat1 libgpm2 libmagic-mgc libmagic1 libmpdec2 libpython3.6...

(3) 保存为新镜像

在新窗口中查看当前运行的容器:

docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
1abe6e7341ca        ubuntu              "/bin/bash"         8 minutes ago       Up 8 minutes                            laughing_leavitt

laughing_leavitt 是 Docker 为我们的容器随机分配的名字。

执行 docker commit 命令将容器保存为镜像:

docker commit laughing_leavitt ubuntu-with-vi
sha256:9d2fac08719de640df6a923bd6c1dc82d73817d29e9c287d024b6cd2a7235683

查看新镜像 ubuntu-with-vi 的属性:

docker images
REPOSITORY          TAG                 IMAGE ID            CREATED              SIZE
ubuntu-with-vi      latest              9d2fac08719d        About a minute ago   169MB
ubuntu              latest              ea4c82dcd15a        2 weeks ago          85.8MB

从 SIZE 属性看到镜像因为安装了软件而变大了。从新镜像启动容器,验证 vi 已经可以使用:

which vim
/usr/bin/vim

以上演示了如何通过 docker commit 创建新镜像。然而,Docker 并不建议用户通过这种方式构建镜像。原因如下:

  • 这是一种手工创建镜像的方式,容易出错,效率低且可重复性弱
  • 更重要的是,使用者并不知道镜像是如何创建出来的,里面是否有恶意程序。也就是说无法对镜像进行审计,存在安全隐患

3.2.2 Dockerfile

Dockerfile 是一个文本文件,记录了镜像构建的所有步骤

第一个 Dockerfile

用 Dockerfile 创建上节的 ubuntu-with-vi,其内容为:

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

下面运行 docker build 命令构建镜像,并分析其细节:

pwd                            (1)
/root  
ls                             (2)   
Dockerfile   
docker build -t ubuntu-with-vi-dockerfile .            (3)   
Sending build context to Docker daemon 32.26 kB        (4)   
Step 1 : FROM ubuntu           (5)   
 ---> f753707788c5   
Step 2 : RUN apt-get update && apt-get install -y vim  (6)   
 ---> Running in 9f4d4166f7e3  (7)   
......   
Setting up vim (2:7.4.1689-3ubuntu1.1) ...   
 ---> 35ca89798937             (8)    
Removing intermediate container 9f4d4166f7e3           (9)   
Successfully built 35ca89798937                        (10)

(1) 当前目录为 /root

(2) Dockerfile 准备就绪。

(3) 运行 docker build 命令,-t 命名新镜像,末尾的 . 表明 build context 为当前目录。Docker 默认会从 build context 中查找 Dockerfile 文件,也可以通过 -f 参数指定 Dockerfile 的位置。

(4) 从这步开始就是镜像真正的构建过程。首先 Docker 将 build context 中的所有文件发送给 Docker daemon。build context 为镜像构建提供所需要的文件或目录。

Dockerfile 中的 ADDCOPY 等命令可以将 build context 中的文件添加到镜像。此例中,build context 为当前目录 /root,该目录下的所有文件和子目录都会被发送给 Docker daemon。

注意不要将多余的文件放到 build context 中,特别不要把 //usr 等目录作为 build context,否则构建过程会相当缓慢甚至失败。

(5) Step 1:执行 FROM,将 Ubuntu 作为 base 镜像。

(6) Step 2:执行 RUN,安装 vi,具体步骤为 (7) (8) (9)。

(7) 启动 ID 为 9f4d4166f7e3 的临时容器,在容器中通过 apt-get 安装 vim。

(8) 安装成功后,将容器保存为镜像,其 ID 为 35ca89798937这一步底层使用的是类似 docker commit 的命令

(9) 删除临时容器 9f4d4166f7e3

(10) 镜像构建成功。

查看镜像分层结构

参考文章

  1. 《每天5分钟玩转 Docker 容器技术》教程目录 | CloudMan