通过commit理解Docker容器结构——定制自己的实验用Docker镜像

我们经常会遇到需要一个临时的测试环境的情景。比如,测试某个软件如何编译、测试编译参数,或者仅仅是探索某个软件包的配置文件写法……等等。这些场景下,如果直接在我们的计算机或VPS上进行,有可能留下一大堆无法收拾的烂摊子,包括编译环境的破坏……

Docker在这时就能发挥出它的威力。通过创建一个容器,就拥有了一个与宿主机完全隔离的实验环境,可以执行几乎任何操作,而完全不用担心对宿主机的影响。一旦出错,简单地docker rm -f即可。

制作需要的镜像

Docker Hub上默认的操作系统镜像都是最小化镜像,没有任何我们平时需要的工作环境。为此,需要定制镜像。例如我平常需要的工作环境就有:

  • oh-my-zsh
  • 各种编译工具,如gcc
  • ping、traceroute、mtr等网络工具
  • 编辑器,如vim
  • 监控工具,如htop

……
你也可以尝试列一个清单,看看究竟有哪些工具是自己需要的。

一般推荐用Dockerfile的方法定制镜像,但是由于我需要用到的oh-my-zsh没有好的自动化安装方法(用Dockerfile定制时会出错),所以只能应用docker commit操作来定制。

登陆到你的VPS,或者打开安装了docker daemon的计算机终端,执行:

1
docker pull ubuntu

下载官方纯净操作系统镜像。你可以根据喜好下载其他系统的镜像。

执行:

1
docker run --name exp -it ubuntu /bin/bash

解释一下所用到的参数。run指令指示docker新建一个容器并执行对应的命令,特别值得一提的是,如果指定本机不存在的镜像,docker将尝试到指定的registry(一般是Docker Hub)上下载。–name参数指定容器名,在commit时要用到。-i参数指定将标准输入流绑定到终端,-t参数分配一个新的TTY。ubuntu是镜像名。/bin/bash则是要求容器执行的命令。因为我们需要使用容器内的命令行环境,所以运行bash。

这时你就进入到了容器内的bash,可以尽情安装自己需要的软件包了。先执行:

1
apt update -y

更新缓存,然后根据需求安装自己的软件包。

安装完成后,编辑/etc/resolv.conf文件(注:默认的镜像指定了一个很诡异的DNS,在这个DNS上大量域名无法解析):

1
vim /etc/resolv.conf

你也可以根据自己的需要使用别的编辑器。改为:

1
2
nameserver 8.8.8.8
nameserver 8.8.4.4

最后,提交容器所作的修改成为新的镜像。执行:

1
2
3
4
5
docker stop exp
docker commit -c 'VOLUME /data' \
-c 'CMD /bin/zsh' \
exp \
exp:latest

等待一段时间,即可用docker images看到自己制作的镜像。

上述命令中,exp是你刚刚创建容器时指定的容器名,但exp:latest是镜像名称,格式为镜像名:标签,一个镜像有多个标签,相当于一个软件有多个版本,默认是latest。在commit时,我用了两次-c参数,这个参数可以使用多次,每个的参数值都是一条Dockerfile指令。第一条指示docker在由该镜像创建容器时,自动在容器的/data目录下挂载一个新数据卷。第二条设定该容器默认执行的命令,如果你不用zsh,也可以要求它默认使用bash。

以后,只需要执行

1
docker run --name test -it [--rm] exp

就可以新建测试用容器并进入zsh环境中了。甚至你都可以省略–name参数。你还可以可选地加上–rm参数,这样容器退出后就能自动删除,省去删除测试环境的麻烦。

commit的原理

但是我们不推荐使用commit来定制镜像。要了解原因,必须了解docker的存储机制:Union FS。

Union FS是一种分层文件系统。每个docker镜像和容器都由多层组成。因为是分层存储,可以避免重复文件。如下图:

这样,如果多个镜像包含相同的层(根据sha256识别),就不会重复下载这些层,从而很好地利用存储空间。每当下载一个镜像时,也会从第一层开始,逐层下载构建,容器类似。例如一个典型的下载过程:

这是我下载推送到Docker Hub上的自制实验镜像时的情景(在另一台VPS上)。可以看到,由于先前下载过ubuntu镜像,这个基于ubuntu镜像制作的镜像只需要额外下载一层,就可以使用了。

正是因为镜像采用Union FS,每当启动一个新容器时,在镜像的基础上,会加上一层容器存储层,这一层的生命周期和容器相同,一旦容器被删除,这一层的内容也随之消亡。docker commit命令的作用,就是保存这一层的内容,形成新的镜像。但Union FS中,在当前层无法删除上一层的文件,仅仅只是在当前层做一个标记而已。因此,必须确保每一层都只包含必要的内容,否则冗余的文件就会如影随形地跟着我们的自制镜像。因此才推荐用Dockerfile定制,并且一定要加上清理操作。

发布制作的镜像

现在介绍一下如何将自己的定制镜像推送到Docker Hub的镜像仓库里,这样以后只需要用docker pull就能使用自己的镜像了。

打开Docker Hub

在上图所示的位置填写用户名、邮箱、密码。然后在所填写的邮箱中找到激活邮件,激活账户。需要注意的是,Docker Hub网站可以使用邮件/用户名+密码登陆。

登陆到后台后,点击Create Repository按钮,如下图

分别输入镜像仓库名称、短描述、完整描述(短描述最多100个字符,完整描述可以不写)。

最后要设置镜像仓库的可见性。设置为public是公开镜像,意味着任何人都可以下载使用。设置为private是私有镜像,必须先用docker login登陆后才能下载,直接下载会显示找不到镜像。每个账号有一个免费的private镜像配额。(注意,一个仓库一般用于存放同一镜像的不同版本,如果你想上传更多镜像,需要创建多个仓库)

创建仓库后就可以推送你的镜像了。

执行

1
docker images

浏览已有的镜像,找到你需要的镜像的ID,记住前3位即可。

执行

1
docker login

登陆到docker hub,注意这里只能使用用户名+密码登陆。默认登陆到的registry为Docker Hub,其他registry必须手动指定。

执行

1
docker tag <ID前三位> username/reponame

给镜像做标记,说明该镜像指向一个仓库。username是注册docker hub时的用户名,reponame是刚刚建立的仓库名称。

执行

1
docker push username/reponame

推送你的镜像到Docker Hub。

你可以直接使用

1
docker pull fourstring/exp

来使用我制作的实验环境镜像。预装软件包有:

  • oh-my-zsh
  • git
  • curl
  • wget
  • screen
  • build-essential
  • vim
  • mtr
  • ping
  • traceroute
  • whois
  • htop
  • [1-16更新] dig

默认挂载数据卷在/data下。这样新建容器时需要指定镜像名为fourstring/exp,如果你觉得麻烦,也可以执行:

1
docker tag 11adecf593ed exp:latest

来将名称exp分配给这个镜像。

参考
《Docker-从入门到实践》