在应用了容器技术的软件开发过程中,控制容器镜像的大小可是一件费时费力的事情。如果我们构建的镜像既是编译软件的环境,又是软件最终的运行环境,这是很难控制镜像大小的。所以常见的配置模式为:分别为软件的编译环境和运行环境提供不同的容器镜像。比如为编译环境提供一个 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. JQuery跳出each循环的方法

    一.jquery each循环,要实现break和continue的功能: break----用return false; continue --用return ture; 二.jquery怎么跳出当 ...

  2. windowsSevice程序和topshelf程序创建服务对比

    文章原地址:http://www.80iter.com/blog/1451523192435464 Topshelf 创建.net服务整理和安装步骤 windowsService和topshelf服务 ...

  3. 20155205 郝博雅 Exp5 MSF基础应用

    20155205 郝博雅 Exp5 MSF基础应用 一.实验目标 本实践目标是掌握metasploit的基本应用方式,重点常用的三种攻击方式的思路.具体需要完成: 1.一个主动攻击实践,如ms08_0 ...

  4. expdp错误案例

    转自:https://www.cnblogs.com/kerrycode/p/3960328.html Oracle数据泵(Data Dump)使用过程当中经常会遇到一些奇奇怪怪的错误案例,下面总结一 ...

  5. STM32CubeMX+Keil裸机代码风格(1)

    1.打开STM32CubeMX,New project 选好自己要用的芯片 2.选上左侧SYS中的debug Serial Wire(定义烧程序的端口) . 3,选上左侧TIM6,使TIM6可用(TI ...

  6. java(一) 基础部分

    1.11.简单讲一下java的跨平台原理 Java通过不同的系统.不同版本.不同位数的java虚拟机(jvm),来屏蔽不同的系统指令集差异而对外体统统一的接口(java API),对于我们普通的jav ...

  7. Exception in thread "main" java.lang.UnsupportedClassVersionError: org/apache/ma ven/cli/Maven

    此异常是由于jdk版本和maven版本不一致导致的 在maven官网上的说明是这样的:Maven 3.3+ require JDK 1.7 or above to execute - they sti ...

  8. Shell输入和输出功能-3

  9. 学习笔记----php环境配置

    Php开发环境自定义搭建 (万事开头难) 第一步:Apache安装(httpd-2.4.37-win64-VC15.zip) 下载已编译apache安装包:Apachelounge官方下载地址:htt ...

  10. python Descriptor (描述符)

    简介: python 描述符是新式类(继承自object)中的语言协议,基于描述符可以提供更佳优雅的解决方案. python的classmethod, staticmethod, property都是 ...