跳至主要内容

4.12 多階段建置映像檔

若是有實際做過上一章的練習後,想必對於建置一個映像檔更有把握了吧?

但這對於建立一個優質的映像檔只是跨出一小步而已,若是用 docker image list 的指令觀看剛剛上一章練習結束的映像檔大小足足有 1.08 GB,對於映像檔沒什麼概念的人可能會想說現在的硬碟又不值錢,1.08 GB 就送它吧。

其實在 Docker 的世界裡 1.08 GB 的映像檔可以說是大的嚇死人,就連運行一個有 postgreSQL 的映像檔都只有 376 MB,就可以想像這個只有一行字的 next.js 大得有多不可思議。

所以有一種方式可以幫助映像檔瘦身,就是使用多階段的建置方式,整個多階段建置的精髓都是在 COPY --from 這段指令,之前的章節使用 COPY 都是從本機複製檔案到映像檔的檔案系統中,而 COPY --from 則可以讓我們從另一個映像檔複製檔案到現階段的映像檔。

我知道這樣聽下來還是霧煞煞,直接來看 Dockerfile 範例。

FROM alpine:3.16.2 AS builder # 建置階段
RUN echo 'Builder' > /example.txt # 建置階段

FROM alpine:3.16.2 AS tester # 測試階段
COPY --from=builder /example.txt /example.txt # 測試階段
RUN echo 'Tester' >> /example.txt # 測試階段

FROM alpine:3.16.2 # 最終階段
COPY --from=tester /example.txt /example.txt # 最終階段CMD [ "cat", "/example.txt" ] # 最終階段

我們將整個 Dockerfile 分成三個階段,這邊有一個簡單的概念,只要是用 FROM 作為開頭就可以說是一個新的階段,而在第一個 FROM 到第二個 FROM 之間的指令結果都會停留在第一個階段中。

建置階段:

首先利用了 alpine:3.16.2 這個映像檔作為基礎,並且簡單的執行了一個 RUN 的指令,作用是把 Builder 這段文字寫入 example.txt 這個檔案,就結束任務了。

測試階段:

這邊 Dockerfile 讀到了第二 FROM,所以就當作一個新的開始,而我們一樣使用 alpine:3.16.2 這個映像檔作為基礎,但不同的是,我們使用了 COPY --from=builder /example.txt /example.txt 這段指令。

對於 Docker 來說,要從 builder 這個階段複製一份 example.txt 到現在這個階段內並命名為 example.txt,此時 Docker 會去找 builder 這個階段,但其實我們已經把第一階段命名好了,可以看到第一個 FROM 的後面我們用了 AS 這個語法,並將第一個階段命名為 builder。

接著再把 Tester 這段文字也寫入 example.txt 檔案中,再來就遇到第三個 FROM 並結束了第二個階段。

最終階段:

來到最後一個階段,我們使用了 COPY --from=tester /example.txt /example.txt 來把 tester 這個階段的 example.txt 複製過來最終階段,並且命名為 example.txt;做的事情其實和第二階段一樣,只是最後使用了 CMD 並且去讀取 example.txt 這個檔案的內容。

讓我們先建置這個映像檔:

$ docker image build --tag example .
[+] Building 5.7s (10/10) FINISHED
=> [internal] load build definition from Dockerfile 0.2s
=> => transferring dockerfile: 313B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/alpine:3.16.2 3.8s
=> [auth] library/ruby:pull token for registry-1.docker.io 0.0s
=> [builder 1/2] FROM docker.io/library/alpine:3.16.2@sha256:bc... 0.0s
=> => resolve docker.io/library/alpine:3.16.2@sha..... 0.0s
=> => sha256:bc41182d7ef5ffc53a40b044e72519.... 0.0s
=> => sha256:1304f174557314a7ed9eddb4eab1..... 0.0s
=> => sha256:9c6f0724472873bb50a2ae67a9e7..... 0.0s
=> [builder 2/2] RUN echo 'Builder' > /example.txt 0.7s
=> [tester 2/3] COPY --from=builder /example.txt /example.txt 0.1s
=> [tester 3/3] RUN echo 'Tester' >> /example.txt 0.4s
=> [stage-2 2/2] COPY --from=tester /example.txt /example.txt 0.1s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:d7b4e571b321c9f72696e3f620b64a2859.... 0.0s
=> => naming to docker.io/library/example 0.0s

接著猜猜看,如果我們把這個映像檔運行成容器會發生什麼事呢?用我們學習到現在的 Docker 基本知識猜猜看吧!

公佈答案囉!

$ docker container run example
Builder
Tester

跟你想得一樣嗎?藉由複製前兩個階段的檔案一直傳遞到最後一個階段,並且讀取檔案中的內容,確實都還保留著前兩個階段所寫入的文字。

這代表什麼呢?代表著我們能夠在前面的階段將要安裝的套件以及安裝套件所需的編譯工具準備好,並且安裝完應用程式所需的套件,只把安裝好的套件複製到第二個階段,這將會把第一個階段編譯所需要的工具都丟棄,也大幅度地減少了映像檔的大小。

接著我們將分別使用編譯式的程式語言 Golang 以及直譯式的程式語言 JavaScript 做示範,來看看多階段建置到底有多少的差異!