Skip to content

Latest commit

 

History

History
5101 lines (3589 loc) · 31.7 KB

42 小即是美?构建最小Go程序容器镜像.md

File metadata and controls

5101 lines (3589 loc) · 31.7 KB

42 小即是美?构建最小Go程序容器镜像

小即是美?构建最小Go程序容器镜像

自从 2013 年 dotCloud 公司 (现已改名为 Docker Inc) 发布 Docker 容器技术以来,到目前为止已经有 7 年多的时间了。这期间 Docker 技术飞速发展,催生出一个生机勃勃的、以轻量级容器技术为基础的庞大的容器平台生态圈,开启了云原生计算时代,并推动了轻量级容器技术成为云原生时代的核心支撑技术。

作为 Docker 三大核心技术之一的轻量级容器镜像技术在 Docker 的快速发展之路上可谓功不可没:镜像让容器真正插上了翅膀,实现了容器自身的重用和标准化传播,使得开发、交付、运维流水线上的各个角色真正围绕同一交付物,“test what you write, ship what you test” 成为现实。

对于已经接纳和使用 Docker 技术在日常开发工作中的开发者而言,构建 Docker 镜像已经是家常便饭。但如何更高效地构建以及构建出 Size 更小的镜像却是很多 Docker 技术初学者心中常见的疑问,甚至是一些老手都未曾细致考量过的问题。小即是美!小镜像打包快、下载快、启动快、占用资源小以及受攻击面小,对于任何语言的开发者来说,都是十分具有吸引力的。Go 语言已经成为云原生时代的头部主流语言,在本节中我们就来看看如何一步步的构建出最小 Go 程序容器镜像。

1. 镜像:继承中的创新

谈镜像构建之前,我们先来简要说下镜像

Docker 技术本质上并不是新技术,而是将已有技术进行了更好地整合和包装。内核容器技术以一种完整形态最早出现在 Sun 公司Solaris 操作系统上, Solaris 是当时最先进的服务器操作系统。2005 年 Sun 发布了 Solaris Container 技术,从此开启了内核容器之门。

2008 年,以 Google 公司开发人员为主导实现的 Linux Container (即 LXC) 功能在被 merge 到 Linux 内核中。LXC 是一种内核级虚拟化技术,主要基于 NamespacesCgroups 技术,实现共享一个操作系统内核前提下的进程资源隔离,为进程提供独立的虚拟执行环境,这样的一个虚拟的执行环境就是一个容器。本质上说,LXC 容器与现在的 Docker 所提供容器是一样的。Docker 也是基于 Namespaces 和 Cgroups 技术之上实现的,Docker 的 创新之处 在于其基于 Union File System 技术定义了一套容器打包规范,真正将容器中的应用及其运行的所有依赖都封装到一种特定格式的文件中去,而这种文件就被称为 镜像(即 image),原理见下图(引自 Docker 官网): 42 小即是美?构建最小Go程序容器镜像

图 10-2-1:Docker 镜像原理

镜像是容器的 “序列化” 标准,这一创新为容器的存储、重用和传输奠定了基础。并且 “坐上了巨轮” 的容器镜像可以传播到世界每一个角落,这无疑助力了容器技术的飞速发展。

Solaris Container、LXC 等早期内核容器技术不同,Docker 为开发者提供了开发者体验良好的工具集,这其中就包括了用于镜像构建的 Dockerfile 以及一种用于编写 Dockerfile 领域特定语言。采用 Dockerfile 方式构建成为镜像构建的标准方法,其可重复、可自动化、可维护以及分层精确控制等特点是采用传统采用 docker commit 命令提交的镜像所不能比拟的。

2. “镜像是个筐”:初学者的认知

“镜像是个筐,什么都往里面装” - 这句俏皮话可能是大部分 Docker 初学者对镜像最初认知的真实写照。这里我们用一个例子来生动地展示一下。我们将 httpserver.go 这个源文件编译为 httpd 程序并通过镜像发布,考虑到被编译的源码并非本文重点,这里使用了一个极简的示例代码:

// sources/tiny-image/httpserver.go

package main

import (
        "fmt"
        "net/http"
)

func main() {
        fmt.Println("http daemon start")
        fmt.Println("  -> listen on port:8080")
        http.ListenAndServe(":8080", nil)
} 

1 2 3 4 5 6 7 8 9 10 11 12 13 14

1 2 3 4 5 6 7 8 9 10 11 12 13 14

1 2 3 4 5 6 7 8 9 10 11 12 13 14

1 2 3 4 5 6 7 8 9 10 11 12 13 14

1 2 3 4 5 6 7 8 9 10 11 12 13 14

1 2 3 4 5 6 7 8 9 10 11 12 13 14

1 2 3 4 5 6 7 8 9 10 11 12 13 14

1 2 3 4 5 6 7 8 9 10 11 12 13 14

1 2 3 4 5 6 7 8 9 10 11 12 13 14

1 2 3 4 5 6 7 8 9 10 11 12 13 14

1 2 3 4 5 6 7 8 9 10 11 12 13 14

1 2 3 4 5 6 7 8 9 10 11 12 13 14

1 2 3 4 5 6 7 8 9 10 11 12 13 14

1 2 3 4 5 6 7 8 9 10 11 12 13 14

1 2 3 4 5 6 7 8 9 10 11 12 13 14

1 2 3 4 5 6 7 8 9 10 11 12 13 14

1 2 3 4 5 6 7 8 9 10 11 12 13 14

1 2 3 4 5 6 7 8 9 10 11 12 13 14

1 2 3 4 5 6 7 8 9 10 11 12 13 14

1 2 3 4 5 6 7 8 9 10 11 12 13 14

1 2 3 4 5 6 7 8 9 10 11 12 13 14

1 2 3 4 5 6 7 8 9 10 11 12 13 14

1 2 3 4 5 6 7 8 9 10 11 12 13 14

1 2 3 4 5 6 7 8 9 10 11 12 13 14

1 2 3 4 5 6 7 8 9 10 11 12 13 14

1 2 3 4 5 6 7 8 9 10 11 12 13 14

接下来,我们来编写一个用于构建目标镜像的 Dockerfile

From ubuntu:14.04

RUN apt-get update \
      && apt-get install -y software-properties-common \
      && add-apt-repository ppa:gophers/archive \
      && apt-get update \
      && apt-get install -y golang-1.9-go \
                            git \
      && rm -rf /var/lib/apt/lists/*

ENV GOPATH /root/go
ENV GOROOT /usr/lib/go-1.9
ENV PATH="/usr/lib/go-1.9/bin:${PATH}"

COPY ./httpserver.go /root/httpserver.go
RUN go build -o /root/httpd /root/httpserver.go \
      && chmod +x /root/httpd

WORKDIR /root
ENTRYPOINT ["/root/httpd"] 

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

构建这个镜像:

$ docker build -t repodemo/httpd:latest .
//...构建输出这里省略...

$ docker images
REPOSITORY                       TAG                 IMAGE ID            CREATED             SIZE
repodemo/httpd                   latest              183dbef8eba6        2 minutes ago       550MB
ubuntu                           14.04               dea1945146b9        2 months ago        188MB 

1 2 3 4 5 6 7

1 2 3 4 5 6 7

1 2 3 4 5 6 7

1 2 3 4 5 6 7

1 2 3 4 5 6 7

1 2 3 4 5 6 7

1 2 3 4 5 6 7

1 2 3 4 5 6 7

1 2 3 4 5 6 7

1 2 3 4 5 6 7

1 2 3 4 5 6 7

1 2 3 4 5 6 7

1 2 3 4 5 6 7

1 2 3 4 5 6 7

1 2 3 4 5 6 7

1 2 3 4 5 6 7

1 2 3 4 5 6 7

1 2 3 4 5 6 7

1 2 3 4 5 6 7

1 2 3 4 5 6 7

1 2 3 4 5 6 7

1 2 3 4 5 6 7

1 2 3 4 5 6 7

1 2 3 4 5 6 7

1 2 3 4 5 6 7

1 2 3 4 5 6 7

整个镜像的构建过程因环境而定。如果您的网络速度一般,这个构建过程可能会花费你 10 多分钟甚至更多。最终如我们所愿,基于 repodemo/httpd:latest 这个镜像的容器可以正常运行:

$ docker run repodemo/httpd
http daemon start
  -> listen on port:8080 

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

我们通过一个 Dockerfile 构建出一个镜像。Dockerfile 由若干条命令 (Command) 组成,每个命令 (Command) 的执行结果都会单独形成一个层 (layer)。我们来探索一下构建出来的镜像:

$ docker history 183dbef8eba6
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
183dbef8eba6        21 minutes ago      /bin/sh -c #(nop)  ENTRYPOINT ["/root/httpd"]   0B
27aa721c6f6b        21 minutes ago      /bin/sh -c #(nop) WORKDIR /root                 0B
a9d968c704f7        21 minutes ago      /bin/sh -c go build -o /root/httpd /root/h...   6.14MB
... ...
aef7700a9036        30 minutes ago      /bin/sh -c apt-get update       && apt-get...   356MB
.... ...
<missing>           2 months ago        /bin/sh -c #(nop) ADD file:8f997234193c2f5...   188MB 

1 2 3 4 5 6 7 8 9

1 2 3 4 5 6 7 8 9

1 2 3 4 5 6 7 8 9

1 2 3 4 5 6 7 8 9

1 2 3 4 5 6 7 8 9

1 2 3 4 5 6 7 8 9

1 2 3 4 5 6 7 8 9

1 2 3 4 5 6 7 8 9

1 2 3 4 5 6 7 8 9

1 2 3 4 5 6 7 8 9

1 2 3 4 5 6 7 8 9

1 2 3 4 5 6 7 8 9

1 2 3 4 5 6 7 8 9

1 2 3 4 5 6 7 8 9

1 2 3 4 5 6 7 8 9

1 2 3 4 5 6 7 8 9

1 2 3 4 5 6 7 8 9

1 2 3 4 5 6 7 8 9

1 2 3 4 5 6 7 8 9

1 2 3 4 5 6 7 8 9

1 2 3 4 5 6 7 8 9

1 2 3 4 5 6 7 8 9

1 2 3 4 5 6 7 8 9

1 2 3 4 5 6 7 8 9

1 2 3 4 5 6 7 8 9

1 2 3 4 5 6 7 8 9

我们去除掉那些 Size 为 0 或很小的层 (layer),我们看到三个 Size 占比较大的层,见下图: 42 小即是美?构建最小Go程序容器镜像

图 10-2-2:Docker 镜像分层探索

虽然 Docker 引擎利用缓存机制可以让同主机下非首次的镜像构建执行得很快,但是在 Docker 技术热情催化下的这种构建思路让 docker 镜像在存储和传输方面的优势荡然无存,要知道一个 ubuntu-server 16.04 的虚拟机 ISO 文件的大小也就不过 600 多 MB 而已。

3. “理性的回归”:builder 模式的崛起

Docker 使用者在新技术接触初期的热情 “冷却” 之后迎来了 “理性的回归”。根据上面分层镜像的图示,我们发现最终镜像中包含构建环境是多余的,我们只需要在最终镜像中包含足够支撑 httpd 应用运行的运行环境即可,而 base image 自身就可以满足。于是我们应该去除不必要的中间层:

42 小即是美?构建最小Go程序容器镜像

图 10-2-3:去除不必要的中间层

现在问题来了!如果不在同一镜像中完成应用构建,那么在哪里、由谁来构建应用呢?至少有两种方法:

  • 在本地构建并拷贝到镜像中;
  • 借助构建者镜像 (builder image) 构建。

方法 1 的本地构建有很多局限性,比如:本地环境无法复用、无法很好融入持续集成 / 持续交付流水线等。借助 builder image 进行构建已经成为 Docker 社区的一个最佳实践,Docker 官方为此也推出了各种主流编程语言的官方 base image,比如:go、java、node.js、python 以及 ruby 等。借助 builder image 进行镜像构建的流程原理如下图: 42 小即是美?构建最小Go程序容器镜像

图 10-2-4:借助 builder image 进行镜像构建的流程图

通过原理图,我们可以看到整个目标镜像的构建被分为了两个阶段:

  1. 第一阶段:构建负责编译源码的构建者镜像;
  2. 第二阶段:将第一阶段的输出作为输入,构建出最终的目标镜像。

我们选择 golang:1.9.2 作为 builder base image,构建者镜像的 Dockerfile 如下:

// sources/tiny-image/Dockerfile.build

FROM golang:1.9.2

WORKDIR /go/src
COPY ./httpserver.go .

RUN go build -o httpd ./httpserver.go 

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

执行构建:

$ docker build -t repodemo/httpd-builder:latest -f Dockerfile.build . 

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

构建好的应用程序 httpd 放在了镜像 repodemo/httpd-builder 中的 /go/src 目录下,我们需要一些 “胶水” 命令来连接两个构建阶段,这些命令将 httpd 从构建者镜像中取出并作为下一阶段构建的输入:

$ docker create --name extract-httpserver repodemo/httpd-builder
$ docker cp extract-httpserver:/go/src/httpd ./httpd
$ docker rm -f extract-httpserver
$ docker rmi repodemo/httpd-builder 

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

通过上面的命令,我们将编译好的 httpd 程序拷贝到了本地。下面是目标镜像的 Dockerfile:

// sources/tiny-image/Dockerfile.target
From ubuntu:14.04

COPY ./httpd /root/httpd
RUN chmod +x /root/httpd

WORKDIR /root
ENTRYPOINT ["/root/httpd"] 

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

1 2 3 4 5 6 7 8

接下来我们来构建目标镜像:

$ docker build -t repodemo/httpd:latest -f Dockerfile.target . 

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

我们来看看这个镜像的 “体格”:

$ docker images
REPOSITORY                       TAG                 IMAGE ID            CREATED             SIZE
repodemo/httpd                   latest              e3d009d6e919        12 seconds ago      200MB 

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

1 2 3

200MB!目标镜像的 Size 降为原来的 1/2 还多。

4. “像赛车那样减去所有不必要的东西”:追求最小镜像

前面我们构建出的镜像的 Size 已经缩小到 200MB,但这还不够。200MB 的 “体格” 在我们的网络环境下缓存和传输仍然很难令人满意。我们要为镜像进一步减重,减到尽可能的小,就像赛车那样,为了能减轻重量将所有不必要的东西都拆除掉:我们仅保留能支撑我们的应用运行的必要库、命令,其余的一律不纳入目标镜像。当然不仅仅是 Size 上的原因,小镜像还有额外的好处,比如:内存占用小,启动速度快,更加高效;不会因其他不必要的工具、库的漏洞而被攻击,减少了 “攻击面”,更加安全。

42 小即是美?构建最小Go程序容器镜像

图 10-2-5:目标镜像还能更小些吗?

一般应用开发者不会从 scratch 镜像从头构建自己的 base image 以及目标镜像的,开发者会挑选适合的 base image。一些 “蝇量级” 甚至是 “草量级” 的官方 base image 的出现为这种情况提供了条件。

42 小即是美?构建最小Go程序容器镜像

图 10-2-6:一些 base image 的 Size 比较 (来自 imagelayers.io 截图)

从图中看,我们有两个选择:busyboxalpine

单从 image 的 size 上来说,busybox 更小。不过 busybox 默认的 C 运行时库 (libc) 实现是 uClibc,而我们通常运行环境使用的 libc 实现都是 glibc,因此我们要么选择静态编译程序,要么使用 busybox:glibc 镜像作为 base image。

alpine image 是另外一种蝇量级 base image,它使用了比 glibc 更小更安全的 musl libc 库。不过和 busybox image 相比,alpine image 体格还是略大。除了因为 musl 比 uClibc 大一些之外,alpine 还在镜像中添加了自己的包管理系统 apk,开发者可以使用 apk 在基于 alpine 的镜像中添加需要的包或工具。因此,对于普通开发者而言,alpine image 显然是更佳的选择。不过 alpine 使用的 libc 实现为 musl 与基于 glibc 上编译出来的应用程序不兼容。如果直接将前面构建出的 httpd 应用塞入 alpine,在容器启动时会遇到下面错误,因为加载器找不到 glibc 这个动态共享库文件:

standard_init_linux.go:185: exec user process caused "no such file or directory" 

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

1

对于 Go 应用来说,我们可以采用静态编译的程序,但一旦采用静态编译,也就意味着我们将失去一些 libc 提供的原生能力,比如:在 linux 上,你无法使用系统提供的 DNS 解析能力,只能使用 Go 自实现的 DNS 解析器。

我们还可以采用基于 alpine 的 builder image,golang base image 就提供了 alpine 版本。 我们就用这种方式构建出一个基于 alpine base image 的极小目标镜像。 42 小即是美?构建最小Go程序容器镜像

图 10-2-7:借助 alpine builder image 进行镜像构建的流程图

我们新建两个用于 alpine 版本目标镜像构建的 Dockerfile:Dockerfile.build.alpineDockerfile.target.alpine

// sources/tiny-image/Dockerfile.build.alpine
FROM golang:alpine

WORKDIR /go/src
COPY ./httpserver.go .

RUN go build -o httpd ./httpserver.go

// sources/tiny-image/Dockerfile.target.alpine
From alpine

COPY ./httpd /root/httpd
RUN chmod +x /root/httpd

WORKDIR /root
ENTRYPOINT ["/root/httpd"] 

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

构建 builder 镜像:

$ docker build -t repodemo/httpd-alpine-builder:latest -f Dockerfile.build.alpine .

$ docker images
REPOSITORY                       TAG                 IMAGE ID            CREATED              SIZE
repodemo/httpd-alpine-builder    latest              d5b5f8813d77        About a minute ago   275MB 

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

执行 “胶水” 命令:

$ docker create --name extract-httpserver repodemo/httpd-alpine-builder
$ docker cp extract-httpserver:/go/src/httpd ./httpd
$ docker rm -f extract-httpserver
$ docker rmi repodemo/httpd-alpine-builder 

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

1 2 3 4

构建目标镜像:

$ docker build -t repodemo/httpd-alpine -f Dockerfile.target.alpine .

$ docker images
REPOSITORY                       TAG                 IMAGE ID            CREATED             SIZE
repodemo/httpd-alpine            latest              895de7f785dd        13 seconds ago      16.2MB 

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

16.2MB!目标镜像的 Size 降为不到原来的十分之一。我们得到了预期的结果。

5. “要有光,于是便有了光”:对多阶段构建的支持

至此,虽然我们实现了目标 Image 的最小化,但是整个构建过程却是十分繁琐,我们需要准备两个 Dockerfile、需要准备 “胶水” 命令、需要清理中间产物等。作为 Docker 用户,我们希望用一个 Dockerfile 就能解决所有问题,于是就有了 Docker 引擎对多阶段构建 (multi-stage build) 的支持。注意:这个特性非常新,只有 Docker 17.05.0-ce 及以后的版本才能支持。

现在我们就按照 “多阶段构建” 的语法将上面的 Dockerfile.build.alpineDockerfile.target.alpine 合并到一个 Dockerfile 中:

// sources/tiny-image/Dockerfile.multistage

FROM golang:alpine as builder

WORKDIR /go/src
COPY httpserver.go .

RUN go build -o httpd ./httpserver.go

From alpine:latest

WORKDIR /root/
COPY --from=builder /go/src/httpd .
RUN chmod +x /root/httpd

ENTRYPOINT ["/root/httpd"] 

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

Dockerfile 的语法还是很简明和易理解的。即使是你第一次看到这个语法也能大致猜出六成含义。与之前 Dockefile 最大的不同在于在支持多阶段构建的 Dockerfile 中我们可以写多个 “From baseimage” 的语句了,每个 From 语句开启一个构建阶段,并且可以通过 “as” 语法为此阶段构建命名 (比如这里的 builder)。我们还可以通过 COPY 命令在两个阶段构建产物之间传递数据,比如这里传递的 httpd 应用,这个工作之前我们是使用 “胶水” 代码完成的。

构建目标镜像:

$ docker build -t repodemo/httpd-multi-stage -f Dockerfile.multistage .

$ docker images
REPOSITORY                       TAG                 IMAGE ID            CREATED             SIZE
repodemo/httpd-multi-stage       latest              35e494aa5c6f        2 minutes ago       16.2MB 

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

我们看到通过多阶段构建特性构建的 Docker Image 与我们之前通过 builder 模式构建的镜像在效果上是等价的。

6. 小结

沿着时间的轨迹,Docker 镜像构建走到了今天,追求又快又小的镜像已成为了云原生开发者的共识。Go 应用有着 (静态) 编译为单一可执行文件的 “先天特性”,这使得我们可以结合最新容器构建技术为其构建出极小的镜像,使其在云原生生态系统中能发挥出更大的优势,得以更为广泛的应用。