摘要本文主要讨论了对docker build的源码流程进行了梳理和解读,并分享了在制作Dockerfile过程中的一些实践经验,包括如何调试、优化和build中的一些要点。另外,还针对现有Dockerfile的不足进行了简要说明,并分享了对于Dockerfile的一些理解。这是2015年初第一次在社区的微信分享,原文刊载在dockone社区 听众这次的分享主要面向有一定Docker基础的。我希望你已经:
我主要分享一些现在网上或者文档中没有的东西,包括我的理解和一些实践,有误之处也请大家指正。好了,正文开始。 DockerfileDockerfile其实可以看做一个命令集。每行均为一条命令。每行的第一个单词,就是命令command。后面的字符串是该命令所要接收的参数。比如ENTRYPOINT /bin/bash。ENTRYPOINT命令的作用就是将后面的参数设置为镜像的entrypoint。至于现有命令的含义,这里不再详述。DockOne上有很多的介绍。 Docker构建(docker build)docker build的流程docker build的流程(这部分代码基本都在docker/builder中)
在这里,我举一个例子来说明一下在第4步命令的执行过程。以CMD命令为例: func cmd(b *Builder, args []string, attributes map[string]bool, original string) error { cmdSlice := handleJsonArgs(args, attributes) if !attributes["json"] { cmdSlice = append([]string{"/bin/sh", "-c"}, cmdSlice...) } b.Config.Cmd = runconfig.NewCommand(cmdSlice...) if err := b.commit("", b.Config.Cmd, fmt.Sprintf("CMD %q", cmdSlice)); err != nil { return err } if len(args) != 0 { b.cmdSet = true } return nil } 可以看到,b.Config.Cmd = runconfig.NewCommand(cmdSlice...)就是根据传入的CMD,更新了Builder里面的Config。然后进行b.commit。Builder这里的commit大致含义其实与docker/daemon的commit功能大同小异。不过这里commit是包含了以下的一个完整过程(参见internals.go/commit):
不仅仅是CMD命令,几乎所有的命令(除了FROM外),在最后都是使用b.commit来产生一个新的镜像的。 所以这会导致的结果就是,Dockerfile里每一行,最后都会变为镜像中的一层。几乎是有多少有效行,就有多少层。 Dockerfile逆向通过docker history image可以看到该镜像的历史来源。即使没有Dockerfile,也可以通过history来逆向产生Dockerfile。 [root@jd ~]# docker history 2d8 IMAGE CREATED CREATED BY SIZE 2d80e15fcfdb 8 days ago /bin/sh -c #(nop) COPY dir:86faa820e8bf5dcc06 16.29 MB 0f601e909d72 8 days ago /bin/sh -c #(nop) ENTRYPOINT [hack/dind] 0 B 68aed19c5994 8 days ago /bin/sh -c set -x && git clone https://githu 3.693 MB ebc6ef15552b 8 days ago /bin/sh -c #(nop) ENV TOMLV_COMMIT=9baf8a8a9f 0 B fe22e308201a 8 days ago /bin/sh -c set -x && git clone -b v1.0.1 htt 5.834 MB f514c504c9b1 8 days ago /bin/sh -c #(nop) COPY dir:d9a19910e57f47cb3b 3.114 MB e4e3ec8edf1a 8 days ago /bin/sh -c ./contrib/download-frozen-image.sh 1.155 MB 6250561532fa 8 days ago /bin/sh -c #(nop) COPY file:9679abce578bcaa2c 3.73 kB ... 例如0f601e909d72就是由ENTRYPOINT [hack/dind]产生。这里的信息展示的不完全,可以通过docker inspect -f {{.ContainerConfig.Cmd}} layer来看某一层产生的具体信息。 如何做DockerfileDockerfile调试Dockerfile更多的像一个脚本,类似于安装脚本。特别是大篇幅的脚本,想一次写成是比较有难度的。免不了进行一些调试。调试时最好利用Dockerfile的cache功能,可以大幅度节约调试的时间。 举个例子,如果我现在有一个Dockerfile。但是我发现。我还需要再开几个端口,或者再安装其他的软件。这个时候最好不要直接修改已经有的Dockerfile的内容。而是在后面追加命令。这样再build的时候,可以利用已有的cache。 Dockerfile优化调试过后的Dockerfile当然可以作为最终的Dockerfile,提供给用户。但是调试的Dockerfile的缺点就是层数可能过多,而且不易越多。所以最好进行一定的优化和整理。经过整理的Dockerfile生成出来的镜像可以使得层数更少,条理更清晰,也可以更好的复用。 DockerOne里有一篇文章写得很好,可以参考。 这里有两点要强调:
Dockerfile自动build有了Dockerfile,很多人都是在本地build。其实这个是相当耗时的。这个工作其实完全可以交给registry.hub.docker.com来完成。 具体的做法就是:
根据你的Dockerfile内容大小,build时长不确定。但是应该算是比较快了。docker源码的Dockerfile在我本地build了一个多小时。但是registry.hub.docker.com只用了半小时左右。大约是因为外国的月亮比较圆吧。 build完成后,可以在线查看版本信息等。本地需要的话,可以直接pull下来。 国内有多家公司提供了registry.hub.docker.com的Mirror服务,可以直接从国内的源中pull下来。速度快很多。 Dockerfile的不足
其他现在我们回过头来看Docker的分层的另一个可能的用途。 Docker的镜像可以看做是一个软件栈。那么其中有多个软件组成。好了,那么我们是不是可以考虑让软件进行自由叠加呢? 比如:从CentOS镜像上安装了Python形成镜像A,从CentOS镜像上安装了Apache形成镜像B。如果用户想从CentOS上形成一个既有Python又有Apache的镜像,如何做呢? 我想有两种方式,一种是dockerfile的import。我们可以基于镜像A,然后import安装Apache的Dockerfile,从而得到目标镜像。 另外一种是可以直接引入,就是基于镜像A,然后我们直接把B的最后一层(假设B安装apache只形成了一层),搬到镜像A的子层上,不是也可以得到目标镜像么? (责任编辑:IT) |