获取镜像

之前提到过,Docker Hub 上有大量的高质量的镜像可以用,这里我们就说一下怎 么获取这些镜像并运行。
从 Docker Registry 获取镜像的命令是 docker pull。其命令格式为:

  1. docker pull [选项] [Docker Registry地址]<仓库名>:<标签>

具体的选项可以通过docker pull --help 命令看到,这里我们说一下镜像名称 的格式。

  • Docker Registry地址:地址的格式一般是 <域名/IP>[:端口号] 。默认地址是 Docker Hub。
  • 仓库名:如之前所说,这里的仓库名是两段式名称,既 <用户名>/<软件名> 。 对于 Docker Hub,如果不给出用户名,则默认为 library ,也就是官方镜像。

示例
image.png
上面的命令中没有给出 Docker Registry 地址,因此将会从 Docker Hub 获取镜 像。而镜像名称是 ubuntu:14.04 ,因此将会获取官方镜像library/ubuntu 仓库中标签为 14.04 的镜像。
从下载过程中可以看到我们之前提及的分层存储的概念,镜像是由多层存储所构成。
下载也是一层层的去下载,并非单一文件。下载过程中给出了每一层的 ID 的 前 12 位。并且下载结束后,给出该镜像完整的 sha256 的摘要,以确保下载一 致性。

运行

有了镜像后,我们就可以以这个镜像为基础启动一个容器来运行。以上面的 ubuntu:14.04 为例,如果我们打算启动里面的bash并且进行交互式操作的话,可以执行下面的命令。

$ docker run -it --rm ubuntu:14.04 bash
root@e7009c6ce357:/# cat /etc/os-release

NAME="Ubuntu"
VERSION="14.04.5 LTS, Trusty Tahr"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 14.04.5 LTS"
VERSION_ID="14.04"
HOME_URL="http://www.ubuntu.com/"
SUPPORT_URL="http://help.ubuntu.com/"
BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"

root@e7009c6ce357:/# exit
exit
$

docker run就是运行容器的命令,具体格式我们会在后面谈到,我们这 里简要的说明一下上面用到的参数。
-it:这是两个参数,一个是 -i :交互式操作,一个是 -t 终端。我们 这里打算进入 bash 执行一些命令并查看返回结果,因此我们需要交互式终端。
--rm :这个参数是说容器退出后随之将其删除。默认情况下,为了排障需求,退出的容器并不会立即删除,除非手动 docker rm 。我们这里只是随便执行个命令,看看结果,不需要排障和保留结果,因此使用 —rm 可以避免 浪费空间。
ubuntu:14.04 :这是指用 ubuntu:14.04 镜像为基础来启动容器。
bash:放在镜像名后的是命令,这里我们希望有个交互式 Shell,因此用的 是 bash 。
进入容器后,我们可以在 Shell 下操作,执行任何所需的命令。这里,我们执行了cat /etc/os-release,这是 Linux 常用的查看当前系统版本的命令,从返回的结果可以看到容器内是Ubuntu 14.04.5 LTS 系统。 最后我们通过exit退出了这个容器。因为此容器内启动启动的主进程为bash所以退出shell交互式时,容器也会停止。

列出镜像

要想列出已经下载下来的镜像,可以使用 docker images 命令。

$ docker images

REPOSITORY TAG IMAGE ID CREATED SIZE
redis latest 5f515359c7f8 5 days ago 183 MB
nginx latest 05a60462f8ba 5 days ago 181 MB
mongo 3.2 fe9198c04d62 5 days ago 342 MB
<none> <none> 00285df0df87 5 days ago 342 MB
ubuntu 16.04 f753707788c5 4 weeks ago 127 MB
ubuntu latest f753707788c5 4 weeks ago 127 MB
ubuntu 14.04 1e0c3dd64ccd 4 weeks ago 188 MB

列表包含了仓库名、标签、镜像 ID、创建时间以及所占用的空间。 其中仓库名、标签在之前的基础概念章节已经介绍过了。镜像 ID 则是镜像的唯一 标识,一个镜像可以对应多个标签。

不加任何参数的情况下, docker images 会列出所有顶级镜像

列出特定的某个镜像

$ docker images ubuntu

REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu 16.04 f753707788c5 4 weeks ago 127 MB
ubuntu latest f753707788c5 4 weeks ago 127 MB
ubuntu 14.04 1e0c3dd64ccd 4 weeks ago 188 MB

指定仓库名和标签

$ docker images ubuntu:16.04

REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu 16.04 f753707788c5 4 weeks ago 127 MB

过滤器使用

docker images 还支持强大的过滤器参数 —filter ,或者简写 - f 。比如,我们希望看到在 mongo:3.2 之后建立的镜像,可以用下面的命令:

$ docker images -f since=mongo:3.2

REPOSITORY TAG IMAGE ID CREATED SIZE
redis latest 5f515359c7f8 5 days ago 183 MB
nginx latest 05a60462f8ba 5 days ago 181 MB

想查看某个位置之前的镜像也可以,只需要把 since换成before即可。 此外,如果镜像构建时,定义了LABEL ,还可以通过LABEL来过滤。

$ docker images -f label=com.example.version=0.1

以特定格式显示

默认情况下, docker images 会输出一个完整的表格,但是我们并非所有时候都 会需要这些内容。比如,刚才删除虚悬镜像的时候,我们需要利用 docker images 把所有的虚悬镜像的 ID 列出来,然后才可以交给 docker rmi 命令作 为参数来删除指定的这些镜像,这个时候就用到了 -q 参数。

$ docker images -q
5f515359c7f8
05a60462f8ba
fe9198c04d62

--filter配合-q 产生出指定范围的 ID 列表,然后送给另一个 docker 命 令作为参数,从而针对这组实体成批的进行某种操作的做法在 Docker 命令行使用 过程中非常常见
另外一些时候,我们可能只是对表格的结构不满意,希望自己组织列;或者不希望 有标题,这样方便其它程序解析结果等,这就用到了 Go 的模板语法
比如,下面的命令会直接列出镜像结果,并且只包含镜像ID和仓库名:

$ docker images --format "{{.ID}}: {{.Repository}}"

5f515359c7f8: redis
05a60462f8ba: nginx
fe9198c04d62: mongo
00285df0df87: <none>
f753707788c5: ubuntu

或者打算以表格等距显示,并且有标题行,和默认一样,不过自己定义列:

$ docker images --format "table {{.ID}}\t{{.Repository}}\t{{.Tag}}"

IMAGE ID REPOSITORY TAG
5f515359c7f8 redis latest
05a60462f8ba nginx latest
fe9198c04d62 mongo 3.2
00285df0df87 <none> <none>

镜像体积

如果仔细观察,会注意到,这里标识的所占用空间和在 Docker Hub 上看到的镜像 大小不同。比如, ubuntu:16.04 镜像大小,在这里是 127 MB ,但是在 Docker Hub 显示的却是 50 MB 。这是因为 Docker Hub 中显示的体积是压缩后的体积。在镜像下载和上传过程中镜像是保持着压缩状态的,因此 Docker Hub 所显示的大小是网络传输中更关心的流量大小。而 docker images 显示的是镜像下载到本地后,展开的大小,准确说,是展开后的各层所占空间的总和,因为镜像到本地后,查看空间的时候,更关心的是本地磁盘空间占用的大小。
另外一个需要注意的问题是, docker images列表中的镜像体积总和并非是所有镜像实际硬盘消耗。由于 Docker 镜像是多层存储结构,并且可以继承、复用,因此不同镜像可能会因为使用相同的基础镜像,从而拥有共同的层。由于 Docker 使 用 Union FS,相同的层只需要保存一份即可,因此实际镜像硬盘占用空间很可能要比这个列表镜像大小的总和要小的多。

虚悬镜像

上面的镜像列表中,还可以看到一个特殊的镜像,这个镜像既没有仓库名,也没有标签,均为

<none> <none> 00285df0df87 5 days ago 342 MB

这个镜像原本是有镜像名和标签的,原来为 mongo:3.2 ,随着官方镜像维护,发布了新版本后,重新 docker pull mongo:3.2时, mongo:3.2 这个镜像名被转移到了新下载的镜像身上,而旧的镜像上的这个名称则被取消,从而成为了 。除了docker pull 可能导致这种情况, docker build也同样可 以导致这种现象。由于新旧镜像同名,旧镜像名称被取消,从而出现仓库名、标签 均为 的镜像。这类无标签镜像也被称为 虚悬镜像(dangling image) ,可以用下面的命令专门显示这类镜像:

$ docker images -f dangling=true

REPOSITORY TAG IMAGE ID CREATED SIZE
<none> <none> 00285df0df87 5 days ago 342 MB

一般来说,虚悬镜像已经失去了存在的价值,是可以随意删除的,可以用下面的命令删除。

$ docker rmi $(docker images -q -f dangling=true)

中间层镜像

为了加速镜像构建、重复利用资源,Docker 会利用中间层镜像。所以在使用一段时间后,可能会看到一些依赖的中间层镜像。默认的 docker images 列表中只 会显示顶层镜像,如果希望显示包括中间层镜像在内的所有镜像的话,需要加 - a 参数

$ docker images -a

这样会看到很多无标签的镜像,与之前的虚悬镜像不同,这些无标签的镜像很多都是中间层镜像,是其它镜像所依赖的镜像。这些无标签镜像不应该删除,否则会导致上层镜像因为依赖丢失而出错。实际上,这些镜像也没必要删除,因为之前说过,相同的层只会存一遍,而这些镜像是别的镜像的依赖,因此并不会因为它们被 列出来而多存了一份,无论如何你也会需要它们。只要删除那些依赖它们的镜像后,这些依赖的中间层镜像也会被连带删除。

利用 commit 理解镜像构成

镜像是容器的基础,每次执行 docker run的时候都会指定哪个镜像作为容器运行的基础。在之前的例子中,我们所使用的都是来自于 Docker Hub的镜像。直接使用这些镜像是可以满足一定的需求,而当这些镜像无法直接满足需求时,我们就需要定制这些镜像。
镜像是多层存储,每一层是在前一层的基础上进行的修改;而容器同样也是多层存储,是在以镜像为基础层,在其基础上加一层作为容器运行时的存储层。
举个栗子:

docker run --name webserver -d -p 22:80 nginx

这条命令为:将 nginx 镜像启动一个容器,命名为 webserver ,并且将宿主机22端口与容器内部80 端口进行映射,这样我们可以用浏览器通过22端口去访问这个 nginx 服务器。
如果是在 Linux 本机运行的 Docker,或者如果使用的是 Docker for MacDocker for Windows,那么可以直接访问:http://localhost;如果使用的是 Docker Toolbox,或者是在虚拟机云服务器上安装的 Docker,则需要将 localhost 换 为虚拟机地址或者实际云服务器地址。

访问成功既有nginx默认欢迎界面。
image.png

切记一般情况下容器内主进程不应该是bash,因为当主进程退出时容器也会销毁掉,如果使用bash为主进程,退出shell就意味着关闭容器。

所以当我们以nginx为主进程启动容器时,nginx没有交互式终端界面来供我们使用,所以我们可以用docker exec进入容器
假设我们非常不喜欢上面的欢迎页面,我们希望改成欢迎 Docker 的文字,我们可以进入容器,修改其内容。

$ docker exec -it webserver bash

root@3729b97e8226:/# echo '<h1>Hello, Docker!</h1>' > /usr/share
/nginx/html/index.html

root@3729b97e8226:/# exit
exit

我们以交互式终端方式进入 webserver 容器,并执行了 bash命令,也就是获得一个可操作的 Shell
然后,我们用<h1>Hello, Docker!</1>覆盖了 /usr/share/nginx/html/index.html 的内容。
image.png
我们修改了容器的文件,也就是改动了容器的存储层。我们可以通过 docker diff 命令看到具体的改动。

$ docker diff webserver

C /root
A /root/.bash_history
C /run
C /usr
C /usr/share
C /usr/share/nginx
C /usr/share/nginx/html
C /usr/share/nginx/html/index.html
C /var
C /var/cache
C /var/cache/nginx
A /var/cache/nginx/client_temp
A /var/cache/nginx/fastcgi_temp
A /var/cache/nginx/proxy_temp
A /var/cache/nginx/scgi_temp
A /var/cache/nginx/uwsgi_temp

现在定制好了变化,我们希望能将其保存下来形成镜像。
要知道,当我们运行一个容器的时候(如果不使用卷的话),我们做的任何文件修改都会被记录于容器存储层里。而 Docker 提供了一个 docker commit 命令,可以将容器的存储层保存下来成为镜像。换句话说,就是在原有镜像的基础上,再叠加上容器的存储层,并构成新的镜像。以后我们运行这个新镜像的时候,就会拥有原有容器最后的文件变化。
docker commit 的语法格式为:

docker commit [选项] <容器ID或容器名> [<仓库名>[:<标签>]]

我们可以用下面的命令将容器保存为镜像:

$ docker commit \
--author "Tao Wang <twang2218@gmail.com>" \
--message "修改了默认网页" \
webserver \
nginx:v2
sha256:07e33465974800ce65751acc279adc6ed2dc5ed4e0838f8b86f0c87aa
1795214

其中 --author 是指定修改的作者,而--message则是记录本次修改的内容。 这点和 git 版本控制相似,不过这里这些信息可以省略留空。

docker history具体查看镜像内的历史记录,如果比较 nginx:latest 的历史记录,我们会发现新增了我们刚刚提交的这一层。同样在后面通过dockerfile构建镜像时,我们也可以docker history来查看构建过程中各层大小及详细信息

$ docker history nginx:v2

IMAGE CREATED CREATED BYSIZE COMMENT
07e334659748 54 seconds ago nginx -g daemon off;95 B 修改了默认网页
e43d811ce2f4 4 weeks ago /bin/sh -c #(nop) CMD ["nginx" "-g" "daemon 0 B
<missing> 4 weeks ago /bin/sh -c #(nop) EXPOSE 443/tcp 80/tcp 0 B
<missing> 4 weeks ago /bin/sh -c ln -sf /dev/stdout /var/log/nginx/ 22 B
<missing> 4 weeks ago /bin/sh -c apt-key adv --keyserver hkp://pgp. 58.46 MB
<missing> 4 weeks ago /bin/sh -c #(nop) ENV NGINX_VERSION=1.11.5-1 0 B
<missing> 4 weeks ago /bin/sh -c #(nop) MAINTAINER NGINX Docker Ma 0 B
<missing> 4 weeks ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0 B
<missing> 4 weeks ago /bin/sh -c #(nop) ADD file:23aa4f893e3288698c 123 MB

新的镜像定制好后,我们可以来运行这个镜像。

docker run --name web2 -d -p 81:80 nginx:v2

docker commit 命令:手动操作给旧的镜像添加了新的一层,形成新的镜像,对镜像多层存储应该有了更直观的感觉。

慎用docker commit

使用 docker commit 命令虽然可以比较直观的帮助理解镜像分层存储的概念, 但是实际环境中并不会这样使用。
首先,如果仔细观察之前的 docker diff webserver 的结果,你会发现除了真正想要修改的 /usr/share/nginx/html/index.html 文件外,由于命令的执行,还有很多文件被改动或添加了。这还仅仅是最简单的操作,如果是安装软件包、编译构建,那会有大量的无关内容被添加进来,如果不小心清理,将会导致镜像极为臃肿。
此外,使用 docker commit 意味着所有对镜像的操作都是黑箱操作,生成的镜像也被称为黑箱镜像,换句话说,就是除了制作镜像的人知道执行过什么命令、怎么生成的镜像,别人根本无从得知。而且,即使是这个制作镜像的人,过一段时间后也无法记清具体在操作的。虽然 docker diff 或许可以告诉得到一些线索, 但是远远不到可以确保生成一致镜像的地步。这种黑箱镜像的维护工作是非常痛苦的。
而且,之前提及的镜像所使用的分层存储的概念,除当前层外,之前的每一层 都是不会发生改变的,换句话说,任何修改的结果仅仅是在当前层进行标记、添加、修改,而不会改动上一层。如果使用 docker commit 制作镜像,以及后期修改的话,每一次修改都会让镜像更加臃肿一次,所删除的上一层的东西并不会丢失,会一直如影随形的跟着这个镜像,即使根本无法访问到™。这会让镜像更加臃肿。
docker commit 命令除了学习之外,还有一些特殊的应用场合,比如被入侵后保存现场等。但是,不要使用 docker commit 定制镜像,定制行为应该使用Dockerfile 来完成。