[关闭]
@zhaikun 2016-12-28T10:16:56.000000Z 字数 6832 阅读 970

构建镜像

docker


我们已经看到如何拉取已经构建好的带有定制内容的Docker镜像,那么我们如何修改自己的镜像,并更新和管理这些镜像呢?构建Docker镜像有一下两种方法。

用Docker的commit命令创建镜像

创建Docker镜像的第一种方法是使用docker commit命令。可以将此想象为我们是在往版本控制系统里提交变更。我们先创建第一个容器,并在容器里做出修改,就行修改代码一样,最后再将修改提交为一个新镜像。

  1. [root@zzk ~]# docker run -it centos /bin/bash
  2. [root@c726a2e6b783 /]# yum install httpd
  3. Loaded plugins: fastestmirror, ovl
  4. Loading mirror speeds from cached hostfile
  5. * base: mirrors.btte.net
  6. * extras: mirrors.btte.net
  7. * updates: mirrors.btte.net
  8. Resolving Dependencies
  9. --> Running transaction check
  10. ---> Package httpd.x86_64 0:2.4.6-45.el7.centos will be installed
  11. --> Processing Dependency: httpd-tools = 2.4.6-45.el7.centos for package: httpd-2.4.6-45.el7.centos.x86_64
  12. --> Processing Dependency: system-logos >= 7.92.1-1 for package: httpd-2.4.6-45.el7.centos.x86_64
  13. --> Processing Dependency: /etc/mime.types for package: httpd-2.4.6-45.el7.centos.x86_64
  14. --> Processing Dependency: libaprutil-1.so.0()(64bit) for package: httpd-2.4.6-45.el7.centos.x86_64
  15. --> Processing Dependency: libapr-1.so.0()(64bit) for package: httpd-2.4.6-45.el7.centos.x86_64
  16. --> Running transaction check
  17. ---> Package apr.x86_64 0:1.4.8-3.el7 will be installed
  18. ---> Package apr-util.x86_64 0:1.5.2-6.el7 will be installed
  19. .....
  20. [root@c726a2e6b783 /]# exit
  21. exit
  22. [root@zzk ~]# docker ps -a
  23. CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
  24. c726a2e6b783 centos "/bin/bash" 2 minutes ago Exited (0) 27 seconds ago modest_brahmagupta
  25. [root@zzk ~]# docker commit c726a2e6b783 zhaikun1992/httpd
  26. sha256:b9a6d73094f9abfac86517b0bbd12a0f40603e38a9f1828dc42c4d58e8826519
  27. [root@zzk ~]#

我们可以用docker diff命令查看改动

  1. [root@zzk ~]# docker diff modest_brahmagupta
  2. C /usr
  3. C /usr/lib
  4. C /usr/lib/systemd
  5. C /usr/lib/systemd/system
  6. A /usr/lib/systemd/system/httpd.service
  7. A /usr/lib/systemd/system/htcacheclean.service
  8. C /usr/lib/tmpfiles.d
  9. A /usr/lib/tmpfiles.d/httpd.conf
  10. C /usr/sbin
  11. A /usr/sbin/httpd
  12. A /usr/sbin/rotatelogs
  13. A /usr/sbin/fcgistarter
  14. A /usr/sbin/suexec
  15. A /usr/sbin/apachectl
  16. A /usr/sbin/htcacheclean
  17. C /usr/lib64
  18. A /usr/lib64/apr-util-1
  19. A /usr/lib64/libapr-1.so.0
  20. A /usr/lib64/libapr-1.so.0.4.8
  21. A /usr/lib64/libaprutil-1.so.0.5.2
  22. A /usr/lib64/httpd
  23. A /usr/lib64/httpd/modules
  24. A /usr/lib64/httpd/modules/mod_lbmethod_bybusyness.so
  25. A /usr/lib64/httpd/modules/mod_allowmethods.so

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

docker commit 的语法格式为:

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

慎用 docker commit

使用 docker commit 命令虽然可以比较直观的帮助理解镜像分层存储的概念,但是实际环境中并不会这样使用。
首先,如果仔细观察之前的 docker diff modest_brahmagupta 的结果,你会发现除了真正想要安装httpd的 文件外,由于命令的执行,还有很多文件被改动或添加了。这还仅仅是最简单的操作,如果是安装软件包、编译构建,那会有大量的无关内容被添加进来,如果不小心清理,将会导致镜像极为臃肿。
此外,使用 docker commit 意味着所有对镜像的操作都是黑箱操作,生成的镜像也被称为黑箱镜像,换句话说,就是除了制作镜像的人知道执行过什么命令、怎么生成的镜像,别人根本无从得知。而且,即使是这个制作镜像的人,过一段时间后也无法记清具体在操作的。虽然 docker diff 或许可以告诉得到一些线索,但是远远不到可以确保生成一致镜像的地步。这种黑箱镜像的维护工作是非常痛苦的。

而且,回顾之前提及的镜像所使用的分层存储的概念,除当前层外,之前的每一层都是不会发生改变的,换句话说,任何修改的结果仅仅是在当前层进行标记、添加、修改,而不会改动上一层。如果使用 docker commit 制作镜像,以及后期修改的话,每一次修改都会让镜像更加臃肿一次,所删除的上一层的东西并不会丢失,会一直如影随形的跟着这个镜像,即使根本无法访问到™。这会让镜像更加臃肿。

docker commit 命令除了学习之外,还有一些特殊的应用场合,比如被入侵后保存现场等。但是,不要使用 docker commit 定制镜像,定制行为应该使用 Dockerfile 来完成。下面的章节我们就来讲述一下如何使用 Dockerfile 定制镜像。‘

Dockerfile构建镜像

推荐使用被成为Dockerfile的定义文件和 docker build命令来构建镜像。Dockerfile使用基本的基于DSL语法的指令来构建一个Docker镜像,之后使用docker build命令基于该Dockerfile中的指令构建一个新的镜像。

  1. mkdir mynginx
  2. cd mynginx
  3. touch Dockerfile
  4. <div class="md-section-divider"></div>

其内容为

  1. FROM nginx
  2. RUN echo '<h1>hello,zzk!</h1>' > /usr/share/nginx/html/index.html
  3. <div class="md-section-divider"></div>

这个Dockerfile很简单,一共就两行,涉及到了两条命令,FRONRUN

FRON 指定基础镜像

所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。就像我们之前运行了一个nginx镜像的容器,再进行修改一样,基础镜像是必须指定的。而FROM就是指定基础镜像,因此一个Dockerfile中的FROM是必备的指令,并且必须是第一条指令。
在 Docker Hub (https://hub.docker.com/explore/) 上有非常多的高质量的官方镜像, 有可以直接拿来使用的服务类的镜像,如 nginx、redis、mongo、mysql、httpd、php、tomcat 等; 也有一些方便开发、构建、运行各种语言应用的镜像,如 node、openjdk、python、ruby、golang 等。 可以在其中寻找一个最符合我们最终目标的镜像为基础镜像进行定制。 如果没有找到对应服务的镜像,官方镜像中还提供了一些更为基础的操作系统镜像,如 ubuntu、debian、centos、fedora、alpine 等,这些操作系统的软件库为我们提供了更广阔的扩展空间。
除了选择现有镜像为基础镜像外,Docker 还存在一个特殊的镜像,名为 scratch。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。

  1. FROM scratch
  2. ...
  3. <div class="md-section-divider"></div>

如果你以 scratch 为基础镜像的话,意味着你不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。

不以任何系统为基础,直接将可执行文件复制进镜像的做法并不罕见,比如 swarmcoreos/etcd。对于 Linux 下静态编译的程序来说,并不需要有操作系统提供运行时支持,所需的一切库都已经在可执行文件里了,因此直接 FROM scratch 会让镜像体积更加小巧。使用 GO 语言 开发的应用很多会使用这种方式来制作镜像,这也是为什么有人认为 Go 是特别适合容器微服务架构的语言的原因之一。

RUN 执行命令

RUN 指令是用来执行命令行命令的。由于命令行的强大能力,RUN 指令在定制镜像时是最常用的指令之一。其格式有两种:

既然 RUN 就像 Shell 脚本一样可以执行命令,那么我们是否就可以像 Shell 脚本一样把每个命令对应一个 RUN 呢?比如这样:

  1. FROM centos
  2. RUN apt-get update
  3. RUN apt-get install -y gcc libc6-dev make
  4. RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-3.2.5.tar.gz"
  5. RUN mkdir -p /usr/src/redis
  6. RUN tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1
  7. RUN make -C /usr/src/redis
  8. RUN make -C /usr/src/redis install

之前说过,Dockerfile 中每一个指令都会建立一层,RUN 也不例外。每一个 RUN 的行为,就和刚才我们手工建立镜像的过程一样:新建立一层,在其上执行这些命令,执行结束后,commit 这一层的修改,构成新的镜像。

而上面的这种写法,创建了 7 层镜像。这是完全没有意义的,而且很多运行时不需要的东西,都被装进了镜像里,比如编译环境、更新的软件包等等。结果就是产生非常臃肿、非常多层的镜像,不仅仅增加了构建部署的时间,也很容易出错。 这是很多初学 Docker 的人常犯的一个错误。

  1. Union FS 是有最大层数限制的,比如 AUFS,曾经是最大不得超过 42 层,现在是不得超过 127 层。

上面的 Dockerfile 正确的写法应该是这样:

  1. FROM centos
  2. RUN buildDeps='gcc libc6-dev make' \
  3. && apt-get update \
  4. && apt-get install -y $buildDeps \
  5. && wget -O redis.tar.gz "http://download.redis.io/releases/redis-3.2.5.tar.gz" \
  6. && mkdir -p /usr/src/redis \
  7. && tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \
  8. && make -C /usr/src/redis \
  9. && make -C /usr/src/redis install \
  10. && rm -rf /var/lib/apt/lists/* \
  11. && rm redis.tar.gz \
  12. && rm -r /usr/src/redis \
  13. && apt-get purge -y --auto-remove $buildDeps

首先,之前所有的命令只有一个目的,就是编译、安装 redis 可执行文件。因此没有必要建立很多层,这只是一层的事情。因此,这里没有使用很多个 RUN 对一一对应不同的命令,而是仅仅使用一个 RUN 指令,并使用 && 将各个所需命令串联起来。将之前的 7 层,简化为了 1 层。在撰写 Dockerfile 的时候,要经常提醒自己,这并不是在写 Shell 脚本,而是在定义每一层该如何构建。

并且,这里为了格式化还进行了换行。Dockerfile 支持 Shell 类的行尾添加 \的命令换行方式,以及行首 # #进行注释的格式。良好的格式,比如换行、缩进、注释等,会让维护、排障更为容易,这是一个比较好的习惯。

此外,还可以看到这一组命令的最后添加了清理工作的命令,删除了为了编译构建所需要的软件,清理了所有下载、展开的文件,并且还清理了 apt 缓存文件。这是很重要的一步,我们之前说过,镜像是多层存储,每一层的东西并不会在下一层被删除,会一直跟随着镜像。因此镜像构建时,一定要确保每一层只添加真正需要添加的东西,任何无关的东西都应该清理掉。

很多人初学 Docker 制作出了很臃肿的镜像的原因之一,就是忘记了每一层构建的最后一定要清理掉无关文件。

好了,让我们再回到之前定制的 nginx 镜像的 Dockerfile 来。现在我们明白了这个 Dockerfile 的内容,那么让我们来构建这个镜像吧。
Dockerfile 文件所在目录执行:

  1. [root@zzk ~]# cd mynginx/
  2. [root@zzk mynginx]# docker build -t nginx:v2 .
  3. Sending build context to Docker daemon 2.048 kB
  4. Step 1 : FROM nginx
  5. ---> 19146d5729dc
  6. Step 2 : RUN echo '<h1> hello,zzk!</h1> ' > /usr/share/nginx/html/index.html
  7. ---> Running in 93de89ec4fc0
  8. ---> 5f1dfa6e6fcb
  9. Removing intermediate container 93de89ec4fc0
  10. Successfully built 5f1dfa6e6fcb
  11. [root@zzk mynginx]#

从命令的输出结果中,我们可以清晰的看到镜像的构建过程。在 Step 2 中,如同我们之前所说的那样,RUN 指令启动了一个容器 9cdc27646c7b,执行了所要求的命令,并最后提交了这一层 44aa4490ce2c,随后删除了所用到的这个容器 9cdc27646c7b

这里我们使用了docker build 命令进行镜像构建。其格式为:

  1. docker build [选项] <上下文路径/URL/->

在这里我们指定了最终镜像的名称 -t nginx:v2,构建成功后,我们可以像之前运行 nginx 那样来运行这个镜像,其结果会和 nginx 一样。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注