本文是《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/