今天我将围绕 Docker 核心概念镜像展开,首先重点讲解一下镜像的基本操作,然后介绍一下镜像的实现原理。首先说明,咱们本课时的镜像均指 Docker 镜像。
你是否还记得镜像是什么?我们先回顾一下。
镜像是一个只读的 Docker 容器模板,包含启动容器所需要的所有文件系统结构和内容。简单来讲,镜像是一个特殊的文件系统,它提供了容器运行时所需的程序、软件库、资源、配置等静态数据。即镜像不包含任何动态数据,镜像内容在构建后不会被改变
然后我们来看下如何操作镜像。

镜像操作

Lark20200904-175130.png 图 1 镜像操作 从图中可知,镜像的操作可分为:

  • 拉取镜像,使用docker pull命令拉取远程仓库的镜像到本地 ;
  • 重命名镜像,使用docker tag命令“重命名”镜像 ;
  • 查看镜像,使用docker image lsdocker images命令查看本地已经存在的镜像 ;
  • 删除镜像,使用docker rmi命令删除无用镜像 ;
  • 构建镜像,构建镜像有两种方式。第一种方式是使用docker build命令基于 Dockerfile 构建镜像,也是我比较推荐的镜像构建方式;第二种方式是使用docker commit命令基于已经运行的容器提交为镜像。

下面,我们逐一详细介绍。

拉取镜像

Docker 镜像的拉取使用docker pull命令, 命令格式一般为 docker pull [Registry]/[Repository]/[Image]:[Tag]。

  • Registry 为注册服务器,Docker 默认会从 docker.io 拉取镜像,如果你有自己的镜像仓库,可以把 Registry 替换为自己的注册服务器。
  • Repository 为镜像仓库,通常把一组相关联的镜像归为一个镜像仓库,library为 Docker 默认的镜像仓库。
  • Image 为镜像名称。
  • Tag 为镜像的标签,如果你不指定拉取镜像的标签,默认为latest

例如,我们需要获取一个 busybox 镜像,可以执行以下命令:

busybox 是一个集成了数百个 Linux 命令(例如 curl、grep、mount、telnet 等)的精简工具箱,只有几兆大小,被誉为 Linux 系统的瑞士军刀。我经常会使用 busybox 做调试来查找生产环境中遇到的问题。

  1. $ docker pull busybox
  2. Using default tag: latest
  3. latest: Pulling from library/busybox
  4. 61c5ed1cbdf8: Pull complete
  5. Digest: sha256:4f47c01fa91355af2865ac10fef5bf6ec9c7f42ad2321377c21e844427972977
  6. Status: Downloaded newer image for busybox:latest
  7. docker.io/library/busybox:latest

实际上执行docker pull busybox命令,都是先从本地搜索,如果本地搜索不到busybox镜像则从 Docker Hub 下载镜像。
拉取完镜像,如果你想查看镜像,应该怎么操作呢?

查看镜像

Docker 镜像查看使用docker images或者docker image ls命令。
下面我们使用docker images命令列出本地所有的镜像。

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
nginx               latest              4bb46517cac3        9 days ago          133MB
nginx               1.15                53f3fd8007f7        15 months ago       109MB
busybox             latest              018c9d7b792b        3 weeks ago         1.22MB

如果我们想要查询指定的镜像,可以使用docker image ls命令来查询。

$ docker image ls busybox
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
busybox             latest              018c9d7b792b        3 weeks ago         1.22MB

当然你也可以使用docker images命令列出所有镜像,然后使用grep命令进行过滤。使用方法如下:

$ docker images |grep busybox
busybox             latest              018c9d7b792b        3 weeks ago         1.22MB

“重命名”镜像

如果你想要自定义镜像名称或者推送镜像到其他镜像仓库,你可以使用docker tag命令将镜像重命名。docker tag的命令格式为 docker tag [SOURCE_IMAGE][:TAG] [TARGET_IMAGE][:TAG]。
下面我们通过实例演示一下:

$ docker tag busybox:latest mybusybox:latest

执行完docker tag命令后,可以使用查询镜像命令查看一下镜像列表:

docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
busybox             latest              018c9d7b792b        3 weeks ago         1.22MB
mybusybox           latest              018c9d7b792b        3 weeks ago         1.22MB

可以看到,镜像列表中多了一个mybusybox的镜像。但细心的同学可能已经发现,busyboxmybusybox这两个镜像的 IMAGE ID 是完全一样的。为什么呢?实际上它们指向了同一个镜像文件,只是别名不同而已。
如果我不需要mybusybox镜像了,想删除它,应该怎么操作呢?

删除镜像

你可以使用docker rmi或者docker image rm命令删除镜像。
举例:你可以使用以下命令删除mybusybox镜像。

$ docker rmi mybusybox
Untagged: mybusybox:latest

此时,再次使用docker images命令查看一下我们机器上的镜像列表。

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
busybox             latest              018c9d7b792b        3 weeks ago         1.22MB

通过上面的输出,我们可以看到,mybusybox镜像已经被删除。
如果你想构建属于自己的镜像,应该怎么做呢?

构建镜像

构建镜像主要有两种方式:

  1. 使用docker commit命令从运行中的容器提交为镜像;
  1. 使用docker build命令从 Dockerfile 构建镜像。

首先介绍下如何从运行中的容器提交为镜像。我依旧使用 busybox 镜像举例,使用以下命令创建一个名为 busybox 的容器并进入 busybox 容器。

$ docker run --rm --name=busybox -it busybox sh
/ #

执行完上面的命令后,当前窗口会启动一个 busybox 容器并且进入容器中。在容器中,执行以下命令创建一个文件并写入内容:

/ # touch hello.txt && echo "I love Docker. " > hello.txt
/ #

此时在容器的根目录下,已经创建了一个 hello.txt 文件,并写入了 “I love Docker. “。下面,我们新打开另一个命令行窗口,运行以下命令提交镜像:

$ docker commit busybox busybox:hello
sha256:cbc6406aaef080d1dd3087d4ea1e6c6c9915ee0ee0f5dd9e0a90b03e2215e81c

然后使用上面讲到的docker image ls命令查看镜像:

$ docker image ls busybox
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
busybox             hello               cbc6406aaef0        2 minutes ago       1.22MB
busybox             latest              018c9d7b792b        4 weeks ago         1.22MB

此时我们可以看到主机上新生成了 busybox:hello 这个镜像。
第二种方式是最重要也是最常用的镜像构建方式:Dockerfile。Dockerfile 是一个包含了用户所有构建命令的文本。通过docker build命令可以从 Dockerfile 生成镜像。
使用 Dockerfile 构建镜像具有以下特性:

  • Dockerfile 的每一行命令都会生成一个独立的镜像层,并且拥有唯一的 ID;
  • Dockerfile 的命令是完全透明的,通过查看 Dockerfile 的内容,就可以知道镜像是如何一步步构建的;
  • Dockerfile 是纯文本的,方便跟随代码一起存放在代码仓库并做版本管理。

看到使用 Dockerfile 的方式构建镜像有这么多好的特性,你是不是已经迫不及待想知道如何使用了。别着急,我们先学习下 Dockerfile 常用的指令。

| Dockerfile 指令 | 指令简介 | | —- | —- |

| FROM | Dockerfile 除了注释第一行必须是 FROM ,FROM 后面跟镜像名称,代表我们要基于哪个基础镜像构建我们的容器。 |

| RUN | RUN 后面跟一个具体的命令,类似于 Linux 命令行执行命令。 |

| ADD | 拷贝本机文件或者远程文件到镜像内 |

| COPY | 拷贝本机文件到镜像内 |

| USER | 指定容器启动的用户 |

| ENTRYPOINT | 容器的启动命令 |

| CMD | CMD 为 ENTRYPOINT 指令提供默认参数,也可以单独使用 CMD 指定容器启动参数 |

| ENV | 指定容器运行时的环境变量,格式为 key=value |

| ARG | 定义外部变量,构建镜像时可以使用 build-arg = 的格式传递参数用于构建 |

| EXPOSE | 指定容器监听的端口,格式为 [port]/tcp 或者 [port]/udp |

| WORKDIR | 为 Dockerfile 中跟在其后的所有 RUN、CMD、ENTRYPOINT、COPY 和 ADD 命令设置工作目录。 |

看了这么多指令,感觉有点懵?别担心,我通过一个实例让你来熟悉它们。这是一个 Dockerfile:

FROM centos:7
COPY nginx.repo /etc/yum.repos.d/nginx.repo
RUN yum install -y nginx
EXPOSE 80
ENV HOST=mynginx
CMD ["nginx","-g","daemon off;"]

好,我来逐行分析一下上述的 Dockerfile。

  • 第一行表示我要基于 centos:7 这个镜像来构建自定义镜像。这里需要注意,每个 Dockerfile 的第一行除了注释都必须以 FROM 开头。
  • 第二行表示拷贝本地文件 nginx.repo 文件到容器内的 /etc/yum.repos.d 目录下。这里拷贝 nginx.repo 文件是为了添加 nginx 的安装源。
  • 第三行表示在容器内运行yum install -y nginx命令,安装 nginx 服务到容器内,执行完第三行命令,容器内的 nginx 已经安装完成。
  • 第四行声明容器内业务(nginx)使用 80 端口对外提供服务。
  • 第五行定义容器启动时的环境变量 HOST=mynginx,容器启动后可以获取到环境变量 HOST 的值为 mynginx。
  • 第六行定义容器的启动命令,命令格式为 json 数组。这里设置了容器的启动命令为 nginx ,并且添加了 nginx 的启动参数 -g ‘daemon off;’ ,使得 nginx 以前台的方式启动。

上面这个 Dockerfile 的例子基本涵盖了常用的镜像构建指令,代码我已经放在 GitHub上,如果你感兴趣可以到 GitHub 下载源码并尝试构建这个镜像。
学习了镜像的各种操作,下面我们深入了解一下镜像的实现原理。

镜像的实现原理

其实 Docker 镜像是由一系列镜像层(layer)组成的,每一层代表了镜像构建过程中的一次提交。下面以一个镜像构建的 Dockerfile 来说明镜像是如何分层的。

FROM busybox
COPY test /tmp/test
RUN mkdir /tmp/testdir

上面的 Dockerfile 由三步组成:
第一行基于 busybox 创建一个镜像层;
第二行拷贝本机 test 文件到镜像内;
第三行在 /tmp 文件夹下创建一个目录 testdir。
为了验证镜像的存储结构,我们使用docker build命令在上面 Dockerfile 所在目录构建一个镜像:

$ docker build -t mybusybox .

这里我的 Docker 使用的是 overlay2 文件驱动,进入到/var/lib/docker/overlay2目录下使用tree .命令查看产生的镜像文件:

$ tree .
# 以下为 tree . 命令输出内容
|-- 3e89b959f921227acab94f5ab4524252ae0a829ff8a3687178e3aca56d605679
|   |-- diff  # 这一层为基础层,对应上述 Dockerfile 第一行,包含 busybox 镜像所有文件内容,例如 /etc,/bin,/var 等目录
... 此次省略部分原始镜像文件内容
|   `-- link 
|-- 6591d4e47eb2488e6297a0a07a2439f550cdb22845b6d2ddb1be2466ae7a9391
|   |-- diff   # 这一层对应上述 Dockerfile 第二行,拷贝 test 文件到 /tmp 文件夹下,因此 diff 文件夹下有了 /tmp/test 文件
|   |   `-- tmp
|   |       `-- test
|   |-- link
|   |-- lower
|   `-- work
|-- backingFsBlockDev
|-- bec6a018080f7b808565728dee8447b9e86b3093b16ad5e6a1ac3976528a8bb1
|   |-- diff  # 这一层对应上述 Dockerfile 第三行,在 /tmp 文件夹下创建 testdir 文件夹,因此 diff 文件夹下有了 /tmp/testdir 文件夹
|   |   `-- tmp
|   |       `-- testdir
|   |-- link
|   |-- lower
|   `-- work
...

通过上面的目录结构可以看到,Dockerfile 的每一行命令,都生成了一个镜像层,每一层的 diff 夹下只存放了增量数据,如图 2 所示。
Lark20200904-175137.png 图 2 镜像文件系统 分层的结构使得 Docker 镜像非常轻量,每一层根据镜像的内容都有一个唯一的 ID 值,当不同的镜像之间有相同的镜像层时,便可以实现不同的镜像之间共享镜像层的效果。
总结一下, Docker 镜像是静态的分层管理的文件组合,镜像底层的实现依赖于联合文件系统(UnionFS)。充分掌握镜像的原理,可以帮助我们在生产实践中构建出最优的镜像,同时也可以帮助我们更好地理解容器和镜像的关系。

总结

到此,相信你已经对 Docker 镜像这一核心概念有了较深的了解,并熟悉了 Docker 镜像的常用操作(拉取、查看、“重命名”、删除和构建自定义镜像)及底层实现原理。
本课时内容精华,我帮你总结如下:

镜像操作命令:

  1. 拉取镜像,使用 docker pull 命令拉取远程仓库的镜像到本地 ;
  1. 重命名镜像,使用 docker tag 命令“重命名”镜像 ;
  1. 查看镜像,使用 docker image ls 或 docker images 命令查看本地已经存在的镜像;
  1. 删除镜像,使用 docker rmi 命令删除无用镜像 ;
  1. 构建镜像,构建镜像有两种方式。第一种方式是使用 docker build 命令基于 Dockerfile 构建镜像,也是我比较推荐的镜像构建方式;第二种方式是使用 docker commit 命令基于已经运行的容器提交为镜像。

镜像的实现原理:
镜像是由一系列的镜像层(layer )组成,每一层代表了镜像构建过程中的一次提交,当我们需要修改镜像内的某个文件时,只需要在当前镜像层的基础上新建一个镜像层,并且只存放修改过的文件内容。分层结构使得镜像间共享镜像层变得非常简单和方便。

最后试想下,如果有一天我们机器存储空间不足,那你知道使用什么命令可以清理本地无用的镜像和容器文件吗?思考后,可以把你的想法写在留言区。
点击即可查看本课时相关源码


精选评论

*豆:

清理容器多余数据相关的命令有两条,分别是:docker image prune -af 仅仅清除没有被容器使用的镜像文件docker system prune -f 清除多余的数据,包括停止的容器、多余的镜像、未被使用的volume等等

*鑫:

老师关于docker的logs的清理有什么好的方法吗?运行久了发现logs比镜像还大

讲师回复:
可以通过 配置 docker 的 log-opts 参数设置每个容器产生的日志文件大小和数量
**军:

老师 ,对于已经下载的镜像怎么看dockerfile 呢?

讲师回复:
镜像办法直接查看 Dockerfile,不过可以通过 docker history 命令查看镜像构建历史
*凡:

老师,Dockerfile将您的busybox换成了rabbitmq报错误error checking context: ‘file (‘/proc/39633/fd/5’) not found or excluded by .dockerignore’.,网上没找到有用的解决方法,不知道什么问题Dockerfile只有FROM rabbitmqCOPY test /tmp/testRUN mkdir /tmp/testdir麻烦老师帮忙看下,谢谢!

讲师回复:
确认下执行命令的目录下是否有 test 文件
*昕:

请教 :1 如何判断 下载到本地的tag是latest的镜像 对应registry中具体哪个镜像(哪个功能tag,哪个OS/ARCH)。2 Digest 的作用。本地镜像和registry 中 digest不一样 ,对应不上 。

讲师回复:
1. 通常不建议使用latest 作为镜像标签,想要区分镜像是否与仓库一致可以使用镜像 ID 来对比。2. Digest 是 manifest内容的sha256sum
**狗:

如果我想从上面busybox:hello里面copy 那个hello.txt到新build的镜像中,我的Dockerfile如果写成:FROM busybox:helloCOPY /hello.txt /tmp/111.txt运行命令docker build -t xbox .之后得到提示COPY failed: stat /var/lib/docker/tmp/docker-builder074112902/hello.txt: no such file or directory请问这怎解决?

讲师回复:
COPY 命令只能从本地目录拷贝文件,想要从其它镜像拷贝文件,需要用到多阶段构建技术(<br />

multistage-build),我会在 22 课时详细讲解多阶段构建技术

*啸:

请问from这个指令是在哪里执行的,在控制台不行,进入容器的控制台也不行,我的运行环境是docker windows

讲师回复:
FROM 指令是写在 Dockerfile 里的,不是直接执行的命令。
*森:

按照教程docker tag的时候提示no such id,将仓库名称换成镜像ID就可以了,请问老师这正常吗

讲师回复:
因为你本地原来没有 busybox:latest 镜像吧,需要先把这个镜像拉取下来
**大:

删除镜像时出现了,,看上去镜像只是名字被清空了,没有删掉,是啥原因

讲师回复:
可能还有别的容器或者镜像在引用相同的层,如果想强制删除,建议使用 -f 选项
**旺:

/var/lib/docker/overlay2 我竟然没有这个目录

讲师回复:
如果你的文件驱动不是 overlay2 就没有这个目录,Docker 的文件系统我会在 14,15,16 讲详细讲解
**0860:

老师,有疑问,不是说镜像只是只读吗?为什么能修改哦?

讲师回复:
镜像不能修改,docker tag 只是给镜像起个别名
**7885:

请教。link. lower. Work 有什么用

讲师回复:
请问你说的 link. lower. Work 分别指什么?命令还是文件夹?
**俊:

对层的概念不太理解,每一句都是一层?比如host 80也是?这样做的好处是?除了刚才说的共享,说到共享,还想问一下。这个共享是docker会自动共享还是需要dockerfile里面用命令设置

讲师回复:
分层主要就是为了共享,镜像层共享是 Docker 默认的机制,无需设置
**用户1280:

删除镜像:docker rmi或docker image rm 镜像名

*伟:

docker rm -f 容器docker rmi 镜像