六、基於多階段構建減小鏡像體積降低複雜度

本文是《Docker必知必會系列》第六篇,原文發佈於個人博客:悟塵紀

上一篇:Docker必知必會系列(五):Docker 數據持久化存儲與性能調優

一、引言

如何減小所構建鏡像的體積最非常具有挑戰性的事情。Docker 17.05版本以後,新增了Dockerfile多階段構建。所謂多階段構建,實際上是允許一個Dockerfile 中出現多個 FROM 指令。

二、單 Dockerfile 構建鏡像

如果將所有的構建過程都包含在一個 Dockerfile 中,包括項目及其依賴庫的編譯、測試、打包等流程,這樣會帶來的一些問題:

  • 鏡像層次多,鏡像體積較大,部署時間變長
  • 源代碼存在泄露的風險

下面是一個簡單示例:

FROM golang:1.14-alpine
RUN apk --no-cache add git ca-certificates
WORKDIR /go/src/github.com/go/lixl.cn/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/lixl.cn/helloworld/app /root
WORKDIR /root/
CMD ["./app"]

構建鏡像:docker build -t go/helloworld:1 -f Dockerfile1 .

三、Builder 模式構建

爲了解決上面提到的問題,可以採用 Builder 模式:創建兩個 Dockerfile,一個用於開發(包含構建應用程序所需的一切),另一個用於生產(僅包含您的應用程序以及運行該應用程序所需的內容),然後用編譯腳本將其整合:

  • Dockerfile.build 文件:

    FROM golang:1.14-alpine
    RUN apk --no-cache add git ca-certificates
    WORKDIR /go/src/github.com/go/lixl.cn/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 . 
    
  • Dockerfile2 文件:

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

    #!/bin/sh
    echo Building go/helloworld:build
    
    docker build -t go/helloworld:build . -f Dockerfile.build
    docker create --name extract go/helloworld:build
    docker cp extract:/go/src/github.com/go/lixl.cn/helloworld/app ./app
    docker rm -f extract
    
    echo Building go/helloworld:2
    docker build --no-cache -t go/helloworld:2 . -f Dockerfile2
    rm ./app
    

構建鏡像:chmod u+x build.sh && sh ./build.sh

這種方式生成的鏡像會很小,不過過程比較複雜,而且生成的多個鏡像都會佔用系統空間。

四、多階段構建方式

爲了解決這些問題,自 Docker v17.05 開始支持多階段構建。每一條 FROM 指令都是一個構建階段,多條 FROM 就是多階段構建,雖然最後生成的鏡像只能是最後一個階段的結果,但是,能夠將前置階段中的文件拷貝到後邊的階段中,這就是多階段構建的最大意義。示例如下:

FROM golang:1.14-alpine as builder
RUN apk --no-cache add git
WORKDIR /go/src/github.com/go/lixl.cn/helloworld/
RUN go get -d -v github.com/go-sql-driver/mysql
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

FROM alpine:latest as prod
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=0 /go/src/github.com/go/lixl.cn/helloworld/app .
CMD ["./app"]

第二FROM條指令以alpine:latest爲基礎開始新的構建階段。COPY --from=0 行僅將先前階段中構建的工件複製到新階段(第一個FROM條指令的起始編號爲 0),Go SDK 和任何中間工件都不會保存在最終鏡像中。

接下來,使用 docker build -t go/helloworld:3 . 構建鏡像,然後對比三種方式生成的鏡像大小。

docker images
REPOSITORY             TAG           IMAGE ID            CREATED             SIZE
go/helloworld          3             5fb7cd98ef33        2 minutes ago       8.22MB
go/helloworld          2             7c30b66f73f9        2 minutes ago       8.22MB
go/helloworld          1             28fb4443a052        2 hours ago         401MB

可以看出,單 Dockerfile 方式構建的鏡像非常大。後兩種方式構建的鏡像大小一致,但多階段構建大大降低了複雜性。

使用多階段構建:

  • 可以在 Dockerfile 中使用多個 FROM 語句,每個 FROM 指令可以使用不同的基礎鏡像。

  • 每個 FORM 都會開始新的構建階段,可以有選擇地將工件從一個階段複製到另一個階段。

  • 通過在 FROM 指令中添加 AS name 來可以命名階段,然後使用 COPY --from=name 而非數字來引用。

  • 可以指定目標階段來構建鏡像(使用 --target name 指令),而不是必須構建整個 Dockerfile。

  • 可以直接引用外部的鏡像,如:COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf

  • 可以在使用 FROM 指令時引用前一個階段,從上一個階段結束。例如:

    FROM alpine:latest as builder
    RUN apk --no-cache add build-base
    
    FROM builder as build1
    COPY source1.cpp source.cpp
    RUN g++ -o /binary source.cpp
    
    FROM builder as build2
    COPY source2.cpp source.cpp
    RUN g++ -o /binary source.cpp
    

更詳細的介紹,可以參考:https://docs.docker.com/develop/develop-images/multistage-build/

相關文章

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章