在应用了容器技术的软件开发过程中,控制容器镜像的大小可是一件费时费力的事情。如果我们构建的镜像既是编译软件的环境,又是软件最终的运行环境,这是很难控制镜像大小的。所以常见的配置模式为:分别为软件的编译环境和运行环境提供不同的容器镜像。比如为编译环境提供一个 Dockerfile.build,用它构建的镜像包含了编译软件需要的所有内容,比如代码、SDK、工具等等。同时为软件的运行环境提供另外一个单独的 Dockerfile,它从 Dockerfile.build 中获得编译好的软件,用它构建的镜像只包含运行软件所必须的内容。这种情况被称为构造者模式(builder pattern),本文将介绍如何通过 Dockerfile 中的 multi-stage 来解决构造者模式带来的问题。

常见的容器镜像构建过程

比如我们创建了一个 GO 语言编写了一个检查页面中超级链接的程序 app.go(请从 sparkdev 获取本文相关的代码):

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "net/url"
    "os"
    "strings"     "golang.org/x/net/html"
) type scrapeDataStore struct {
    Internal int `json:"internal"`
    External int `json:"external"`
} func isInternal(parsedLink *url.URL, siteUrl *url.URL, link string) bool {
    return parsedLink.Host == siteUrl.Host || strings.Index(link, "#") == 0 || len(parsedLink.Host) == 0
} func main() {
    urlIn := os.Getenv("url")
    if len(urlIn) == 0 {
        urlIn = "https://www.cnblogs.com/"
        log.Fatalln("Need a valid url as an env-var.")
    }     siteUrl, parseErr := url.Parse(urlIn)
    if parseErr != nil {
        log.Fatalln(parseErr)
    }
    resp, err := http.Get(urlIn)
    if err != nil {
        log.Fatalln(err)
    }     scrapeData := &scrapeDataStore{}
    tokenizer := html.NewTokenizer(resp.Body)
    end := false
    for {
        tt := tokenizer.Next()
        switch {
        case tt == html.StartTagToken:
            // fmt.Println(tt)
            token := tokenizer.Token()
            switch token.Data {
            case "a":                 for _, attr := range token.Attr {                     if attr.Key == "href" {
                        link := attr.Val                         parsedLink, parseLinkErr := url.Parse(link)
                        if parseLinkErr == nil {
                            if isInternal(parsedLink, siteUrl, link) {
                                scrapeData.Internal++
                            } else {
                                scrapeData.External++
                            }
                        }                         if parseLinkErr != nil {
                            fmt.Println("Can't parse: " + token.Data)
                        }
                    }
                }
                break
            }
        case tt == html.ErrorToken:
            end = true
            break
        }
        if end {
            break
        }
    }
    data, _ := json.Marshal(&scrapeData)
    fmt.Println(string(data))
}

下面我们通过容器来构建它,并把它部署到生产型的容器镜像中。
首先构建编译应用程序的镜像:

FROM golang:1.7.3
WORKDIR /go/src/github.com/sparkdevo/href-counter/
RUN go get -d -v golang.org/x/net/html
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

把上面的内容保存到 Dockerfile.build 文件中。

接着把构建好的应用程序部署到生产环境用的镜像中:

FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY app .
CMD ["./app"]

把上面的内容保存到 Dockerfile 文件中。

最后需要使用一个脚本把整个构建过程整合起来:

#!/bin/sh
echo Building sparkdevo/href-counter:build
# 构建编译应用程序的镜像
docker build --no-cache -t sparkdevo/href-counter:build . -f Dockerfile.build
# 创建应用程序
docker create --name extract sparkdevo/href-counter:build
# 拷贝编译好的应用程序
docker cp extract:/go/src/github.com/sparkdevo/href-counter/app ./app
docker rm -f extract echo Building sparkdevo/href-counter:latest
# 构建运行应用程序的镜像
docker build --no-cache -t sparkdevo/href-counter:latest .

把上面的内容保存到 build.sh 文件中。这个脚本会先创建出一个容器来构建应用程序,然后再创建最终运行应用程序的镜像。
把 app.go、Dockerfile.build、Dockerfile 和 build.sh 放在同一个目录下,然后进入这个目录执行 build.sh 脚本进行构建。构建后的容器镜像大小:

从上图中我们可以观察到,用于编译应用程序的容器镜像大小接近 700M,而用于生产环境的容器镜像只有 10.3 M,这样的大小在网络间传输的效率是很高的。

运行下面的命令可以检查我们构建的容器是否可以正常的工作:

$ docker run -e url=https://www.cnblogs.com/ sparkdevo/href-counter:latest
$ docker run -e url=http://www.cnblogs.com/sparkdev/ sparkdevo/href-counter:latest

OK,我们写的程序正确的统计了博客园首页和笔者的首页中超级链接的情况。

采用上面的构建过程,我们需要维护两个 Dockerfile 文件和一个脚本文件 build.sh。能不能简化一些呢? 下面我们看看 docker 针对这种情况提供的解决方案:multi-stage。

在 Dockerfile 中使用 multi-stage

multi-stage 允许我们在 Dockerfile 中完成类似前面 build.sh 脚本中的功能,每个 stage 可以理解为构建一个容器镜像,后面的 stage 可以引用前面 stage 中创建的镜像。所以我们可以使用下面单个的 Dockerfile 文件实现前面的需求:

FROM golang:1.7.3
WORKDIR /go/src/github.com/sparkdevo/href-counter/
RUN go get -d -v golang.org/x/net/html
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=0 /go/src/github.com/sparkdevo/href-counter/app .
CMD ["./app"]

把上面的内容保存到文件 Dockerfile.multi 中。这个 Dockerfile 文件的特点是同时存在多个 FROM 指令,每个 FROM 指令代表一个 stage 的开始部分。我们可以把一个 stage 的产物拷贝到另一个 stage 中。本例中的第一个 stage 完成了应用程序的构建,内容和前面的 Dockerfile.build 是一样的。第二个 stage 中的 COPY 指令通过 --from=0 引用了第一个 stage ,并把应用程序拷贝到了当前 stage 中。接下来让我们编译新的镜像:

$ docker build --no-cache -t sparkdevo/href-counter:multi . -f Dockerfile.multi

这次使用 href-counter:multi 镜像运行应用:

$ docker run -e url=https://www.cnblogs.com/ sparkdevo/href-counter:multi
$ docker run -e url=http://www.cnblogs.com/sparkdev/ sparkdevo/href-counter:multi

结果和之前是一样的。那么新生成的镜像有没有特别之处呢:

好吧,从上图我们可以看到,除了 sparkdevo/href-counter:multi 镜像,还生成了一个匿名的镜像。因此,所谓的 multi-stage 不过时多个 Dockerfile 的语法糖罢了。但是这个语法糖还好很诱人的,现在我们维护一个结构简洁的 Dockerfile 文件就可以了!

使用命名的 stage

在上面的例子中我们通过 --from=0 引用了 Dockerfile 中第一个 stage,这样的做法会让 Dockerfile 变得不容易阅读。其实我们是可以为 stage 命名的,然后就可以通过名称来引用 stage 了。下面是改造后的 Dockerfile.mult 文件:

FROM golang:1.7.3 as builder
WORKDIR /go/src/github.com/sparkdevo/href-counter/
RUN go get -d -v golang.org/x/net/html
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /go/src/github.com/sparkdevo/href-counter/app .
CMD ["./app"]

我们把第一个 stage 使用 as 语法命名为 builder,然后在后面的 stage 中通过名称 builder 进行引用 --from=builder。通过使用命名的 stage, Dockerfile 更容易阅读了。

总结

Dockerfile 中的 multi-stage 虽然只是些语法糖,但它确实为我们带来了很多便利。尤其是减轻了 Dockerfile 维护者的负担(要知道实际生产中的 Dockerfile 可不像 demo 中的这么简单)。需要注意的是旧版本的 docker 是不支持 multi-stage 的,只有 17.05 以及之后的版本才开始支持。好了,是不是该去升级你的 docker 版本了?

参考:
Use multi-stage builds
Builder pattern vs. Multi-stage builds in Docker

Dockerfile 中的 multi-stage(多阶段构建)的更多相关文章

  1. Dockerfile 中的 multi-stage

    在应用了容器技术的软件开发过程中,控制容器镜像的大小可是一件费时费力的事情.如果我们构建的镜像既是编译软件的环境,又是软件最终的运行环境,这是很难控制镜像大小的.所以常见的配置模式为:分别为软件的编译 ...

  2. Dockerfile 中的 CMD 与 ENTRYPOINT

    CMD 和 ENTRYPOINT 指令都是用来指定容器启动时运行的命令.单从功能上来看,这两个命令几乎是重复的.单独使用其中的一个就可以实现绝大多数的用例.但是既然 doker 同时提供了它们,为了在 ...

  3. Dockerfile 中的 CMD 与 ENTRYPOINT(转)

    add by zhj:  CMD和ENTRYPOINT的差异很小,可以认为完全可以相互代替.两者都支持shell模式和exec模式, shell模式:跟你在shell下执行命令的格式一样,简单方便,但 ...

  4. Dockerfile多阶段构建原理和使用场景

    本文转载自Dockerfile多阶段构建原理和使用场景 导语 Docker 17.05版本以后,新增了Dockerfile多阶段构建.所谓多阶段构建,实际上是允许一个Dockerfile 中出现多个 ...

  5. Dockerfile多阶段构建

    多阶段构建 之前的做法: 在Docker17.05版本之前,构建Docker镜像,通常采用两种方式: 1.全部放入一个Dockerfile 一种方式是将所有的构建过程全都包含在一个Dockerfile ...

  6. Dockerfile 多阶段构建实践

    写在前面 在Docker Engine 17.05 中引入了多阶段构建,以此降低构建复杂度,同时使缩小镜像尺寸更为简单.这篇小作文我们来学习一下如何编写实现多阶段构建的Dockerfile 关于doc ...

  7. Docker多阶段构建实战(multi-stage builds)

    在编写Dockerfile构建docker镜像时,常遇到以下问题: RUN命令会让镜像新增layer,导致镜像变大,虽然通过&&连接多个命令能缓解此问题,但如果命令之间用到docker ...

  8. 多阶段构建Docker镜像

    在Docker 17.05及更高的版本中支持支持一种全新的构建镜像模式:多阶段构建: 多阶段构建Docker镜像的最大好处是使构建出来的镜像变得更小: 目前常见的两个构建镜像的方式为: 1.直接使用某 ...

  9. Dockerfile 中的 COPY 与 ADD 命令

    Dockerfile 中提供了两个非常相似的命令 COPY 和 ADD,本文尝试解释这两个命令的基本功能,以及其异同点,然后总结其各自适合的应用场景. Build 上下文的概念 在使用 docker ...

随机推荐

  1. JDBC学习(含转载)

    首先连接数据库: Class.forName("com.mysql.jdbc.Driver");//加载jdbc驱动 Connection conn=DriverManager.g ...

  2. NodeJS (npm) 学习笔记

    零, npm是nodeJS的包管理器,下载nodeJS后会自动安装好npm. 一,windows下安装 nodeJS并配置(以及angular安装相关问题) 1, 下载安装 https://nodej ...

  3. gensim和jieba分词进行主题分析,文本相似度

    参考链接:https://blog.csdn.net/whzhcahzxh/article/details/17528261 demo1:结巴分词: # 构造分词库,格式如下: ''' [['楼下', ...

  4. Unity3D编辑器扩展(一)——定义自己的菜单按钮

    Unity3D 引擎的编辑器拥有很强的扩展性,用的好可以帮我们省很多事情.在这里记录下如何去扩展 Unity3D 的编辑器,定制属于我们自己的开发环境. 本篇主要讲解在 Unity3D 引擎的各个窗口 ...

  5. How to setup Visual Studio without pain

    Visual Studio (VS) can be very hard to install. If you are lucky, one whole day may be enough to ins ...

  6. Note of Python Math

    Note of Python Math math 库是Python 提供的内置数学类函数库,而其中复数类型常用于科学计算,一般计算并不常用,因此math 库不支持复数类型.math 库一共提供4个数学 ...

  7. springboot的拦截器Interceptor的性质

    Interceptor在springboot2.x版本的快速入门 实现HandlerInterceptor的接口,并重载它的三个方法:preHandle.postHandle.afterComplet ...

  8. tcp/ip 三次握手和4次挥手

    TCP连接两个端口,谁也发起请求,谁就是client端A,另外一个接受请求的server端B.第一次握手:A向B发一个链接请求,带一个seq = x,SYN= x. 第二次握手:B收到链接请求之后向A ...

  9. iOS开发之ReactiveCocoa下的MVVM

    最近工作比较忙,但还是出来更新博客了,今天给大家分享一些ReactiveCocoa以及MVVM的一些东西,干活还是比较足的.在之前发表过一篇博文,名字叫做<iOS开发之浅谈MVVM的架构设计与团 ...

  10. JavaScript 基础排序的实现(二)

    继上一篇O(n^2)的排序算法后,这一篇主要记录O(n*logn)的排序算法 1.快排(快速排序) 这一算法的核心思想为,先随机选一个数作为标兵或者说是标记(这个数一般来说选择该无序数组的中间那个元素 ...