Dockerfile 中的 multi-stage(多阶段构建)

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

<think>好的,我现在需要回答用户关于Dockerfile和Docker Compose的区别及使用场景的问题。首先,我得回忆一下两者的基本定义和用途。根据引用[3]和[4],Dockerfile是用于构建单个容器镜像的,而Docker Compose则是用来编排多个容器的。用户可能想知道在什么情况下应该使用哪一个,以及它们如何协同工作。 接下来,我需要比较它们的核心功能。Dockerfile通过一系列指令来定义镜像的构建过程,比如FROM、RUN、COPY等。而Docker Compose的YAML文件则定义了服务、网络、卷等,适合多容器应用。这里可以引用[3]提到的“单个容器的构建步骤”和“多容器编排”来强调区别。 然后,使用场景方面,用户可能需要具体例子。比如开发单服务应用时用Dockerfile,而微服务架构需要多个容器协作时用Compose。引用[4]提到Compose允许用一个命令启动整个应用,这很重要。还要注意用户可能提到的性能或管理上的考虑,比如是否应该把所有服务放在一个Dockerfile里,根据引用[1],这可能不是最佳实践,应该分开构建。 另外,用户可能对两者的协同使用感兴趣。例如,在Compose文件中引用多个Dockerfile构建的镜像,这样可以保持模块化。需要提到docker-compose up和build命令的区别,参考引用[2]中的内容,说明不同命令如何影响镜像构建和容器启动。 最后,确保回答结构清晰,分点说明区别、使用场景和协同工作方式。避免技术术语过多,用中文通俗解释。同时生成相关问题,帮助用户深入理解,比如如何组织多服务项目,或优化构建流程的问题。</think>### Dockerfile与Docker Compose的核心区别与使用场景 #### 一、核心定义 1. **Dockerfile** - 是**单个容器镜像的构建蓝图**,通过逐行指令(如 `FROM`, `RUN`, `COPY`)定义镜像的生成过程[^3]。 - 示例:构建一个包含Python环境的镜像 ```dockerfile FROM python:3.8 COPY . /app RUN pip install -r requirements.txt CMD ["python", "app.py"] ``` 2. **Docker Compose** - 是**多容器应用的编排工具**,通过YAML文件(`docker-compose.yml`)定义服务、网络、存储等关联配置[^4]。 - 示例:同时启动Web服务和数据库 ```yaml services: web: build: . ports: ["5000:5000"] db: image: postgres volumes: ["db_data:/var/lib/postgresql/data"] ``` --- #### 二、关键区别对比 | 维度 | Dockerfile | Docker Compose | |--------------|---------------------------------|----------------------------------| | **作用对象** | 单个容器 | 多容器组成的应用系统 | | **核心功能** | 镜像构建 | 服务编排与依赖管理 | | **执行方式** | `docker build -t <镜像名> .` | `docker-compose up` | | **适用场景** | 独立服务或微服务的镜像定制 | 开发/测试环境的多服务联调 | --- #### 三、典型使用场景 1. **优先使用Dockerfile的情况** - 需要定制化基础镜像(如特定软件包安装) - 构建可复用的组件(如公司内部通用Java环境镜像) - 持续集成/交付(CI/CD)中的镜像构建阶段 2. **必须使用Docker Compose的情况** - 多服务依赖(如Web应用+MySQL+Redis) - 需要统一管理网络和存储卷(如容器间共享配置文件) - 快速复现开发环境(团队成员通过`docker-compose up`一键启动) --- #### 四、协同工作模式 在实际项目中,两者通常**结合使用**: 1. 通过Dockerfile为每个服务构建独立镜像 2. 在`docker-compose.yml`中引用这些镜像,并定义服务间的依赖关系 ```yaml services: backend: build: ./backend # 指向包含Dockerfile的目录 depends_on: - db db: image: postgres:13 ``` 3. 使用`docker-compose up --build`同时构建镜像并启动服务[^2] --- #### 五、常见误区 1. **错误实践**:试图用单个Dockerfile构建多个服务 - ❌ 通过多阶段构建multi-stage)生成多个镜像虽然可行,但会降低可维护性[^1] - ✅ 正确做法:每个服务独立Dockerfile + Compose编排 2. **性能陷阱**:`docker-compose build` vs `docker-compose up --build` - 前者强制重新构建镜像,后者仅在镜像不存在时构建[^2] ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值