在 Docker 17.05 版本之前,我们构建 Docker 镜像时,通常会采用两种方式:

全部放入一个 Dockerfile

一种方式是将所有的构建过程编包含在一个 Dockerfile 中,包括项目及其依赖库的编译、测试、打包等流程,这里可能会带来的一些问题:

  • 镜像层次多,镜像体积较大,部署时间变长
  • 源代码存在泄露的风险

例如,编写 app.go 文件,该程序输出 Hello World!

package main

import "fmt"

func main(){
    fmt.Printf("Hello World!");
}

编写 Dockerfile.one 文件

FROM golang:alpine

RUN apk --no-cache add git ca-certificates

WORKDIR /go/src/github.com/go/helloworld/

COPY app.go .

RUN go get -d -v github.com/go-sql-driver/mysql \
  && CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . \
  && cp /go/src/github.com/go/helloworld/app /root

WORKDIR /root/

CMD ["./app"]

构建镜像

$ docker build -t go/helloworld:1 -f Dockerfile.one .

多阶段构建的优势

自 Docker 17.05 版本起,引入了多阶段构建(multi-stage builds)的概念,这种方式能有效解决上述问题。多阶段构建允许我们在一个 Dockerfile 中定义多个 FROM 指令,从而在构建过程中生成多个临时镜像,这样可以减小最终镜像的体积,并避免将源代码暴露在生产环境中。

多阶段构建的示例

以下是一个使用多阶段构建的示例,演示如何构建一个更为精简的 Docker 镜像。

编写 Dockerfile 文件:

# 第一阶段:构建阶段
FROM golang:alpine AS builder

RUN apk --no-cache add git ca-certificates

WORKDIR /go/src/github.com/go/helloworld/

COPY app.go .

# 获取依赖并构建
RUN go get -d -v github.com/go-sql-driver/mysql \
  && CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

# 第二阶段:运行阶段
FROM alpine:latest

# 复制构建好的二进制文件
WORKDIR /root/

COPY --from=builder /go/src/github.com/go/helloworld/app .

# 运行应用程序
CMD ["./app"]

构建镜像

使用以下命令构建镜像:

$ docker build -t go/helloworld:multi .

运行镜像

使用以下命令运行生成的镜像:

$ docker run --rm go/helloworld:multi

总结

多阶段构建 Docker 镜像的方式,不仅可以优化镜像的体积,还能提高构建的安全性,避免将源代码暴露到最终镜像中。通过使用 COPY --from=builder 指令,我们可以轻松地将需要的文件从构建阶段复制到运行阶段,确保最终镜像中仅包含必要的二进制文件和资源。这种构建方式对于构建微服务和容器化应用特别有用,能够提升持续集成和持续部署的效率。