【Docker實戰】Dockerfile多階段構建原理

來源:互聯網
上載者:User

Docker 17.05版本以後,新增了Dockerfile多階段構建。所謂多階段構建,實際上是允許一個Dockerfile 中出現多個 FROM 指令。這樣做有什麼意義呢?

老版本Docker中為什麼不支援多個 FROM 指令

在17.05版本之前的Docker,只允許Dockerfile中出現一個FROM指令,這得從鏡像的本質說起。

在《Docker概念簡介》 中我們提到,你可以簡單理解Docker的鏡像是一個壓縮檔,其中包含了你需要的程式和一個檔案系統。其實這樣說是不嚴謹的,Docker鏡像並非只是一個檔案,而是由一堆檔案組成,最主要的檔案是 

Dockerfile 中,大多數指令會產生一個層,比如下方的兩個例子:

# 樣本一,foo 鏡像的Dockerfile

# 基礎鏡像中已經存在若干個層了

FROM ubuntu:16.04

# RUN指令會增加一層,在這一層中,安裝了 git 軟體

RUN apt-get update \

  && apt-get install -y --no-install-recommends git \

  && apt-get clean \

  && rm -rf /var/lib/apt/lists/*

# 樣本二,bar 鏡像的Dockerfile

FROM foo

# RUN指令會增加一層,在這一層中,安裝了 nginx

RUN apt-get update \

  && apt-get install -y --no-install-recommends nginx \

  && apt-get clean \

  && rm -rf /var/lib/apt/lists/*

假設基礎鏡像 ubuntu:16.04 已經存在5層,使用第一個Dockerfile打包成鏡像 foo,則foo有6層,又使用第二個Dockerfile打包成鏡像bar,則bar中有7層。

如果 ubuntu:16.04 等其他鏡像不算,如果系統中只存在 foo 和 bar 兩個鏡像,那麼系統中一共儲存了多少層呢?

是7層,並非13層,這是因為,foo和bar共用了6層。層的共用機制可以節約大量的磁碟空間和傳輸頻寬,比如你本地已經有了foo鏡像,又從鏡像倉庫中拉取bar鏡像時,只拉取本地所沒有的最後一層就可以了,不需要把整個bar鏡像連根拉一遍。但是層共用是怎樣實現的呢?

原來,Docker鏡像的每一層只記錄檔案變更,在容器啟動時,Docker會將鏡像的各個層進行計算,最後產生一個檔案系統,這個被稱為 聯合掛載。對此感興趣的話可以進入瞭解一下 AUFS。

Docker的各個層是有相關性的,在聯合掛載的過程中,系統需要知道在什麼樣的基礎上再增加新的檔案。那麼這就要求一個Docker鏡像只能有一個起始層,只能有一個根。所以,Dockerfile中,就只允許一個 FROM指令。因為多個 FROM 指令會造成多根,則是無法實現的。但為什麼 Docker 17.05 版本以後允許 Dockerfile支援多個 FROM 指令了呢,莫非已經支援了多根?

多個 FROM 指令的意義

多個 FROM 指令並不是為了產生多根的層關係,最後產生的鏡像,仍以最後一條 FROM 為準,之前的 FROM 會被拋棄,那麼之前的FROM 又有什麼意義呢?

每一條 FROM 指令都是一個構建階段,多條 FROM 就是多階段構建,雖然最後產生的鏡像只能是最後一個階段的結果,但是,能夠將前置階段中的檔案拷貝到後邊的階段中,這就是多階段構建的最大意義。

最大的使用情境是將編譯環境和運行環境分離,比如,之前我們需要構建一個Go語言程式,那麼就需要用到go命令等編譯環境,我們的Dockerfile可能是這樣的:

# Go語言環境基礎鏡像

FROM golang:1.10.3

# 將源碼拷貝到鏡像中

COPY server.go /build/

# 指定工作目錄

WORKDIR /build

# 編譯鏡像時,運行 go build 編譯產生 server 程式

RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOARM=6 go build -ldflags'-w -s'-o server

# 指定容器運行時入口程式 server

ENTRYPOINT  ["/build/server"]

基礎鏡像 golang:1.10.3 是非常龐大的,因為其中包含了所有的Go語言編譯工具和庫,而運行時候我們僅僅需要編譯後的 server 程式就行了,不需要編譯時間的編譯工具,最後產生的大體積鏡像就是一種浪費。

使用脈衝雲的解決辦法是將程式編譯和鏡像打包分開,使用脈衝雲的編譯構建服務,選擇增加構Go語言構建工具,然後在構建步驟中編譯。

最後將編譯介面拷貝到鏡像中就行了,那麼Dockerfile的基礎鏡像並不需要包含Go編譯環境:

# 不需要Go語言編譯環境

FROM  scratch

# 將編譯結果拷貝到容器中

COPY  server /server

# 指定容器運行時入口程式 server

ENTRYPOINT  ["/server"]

提示: scratch 是內建關鍵詞,並不是一個真實存在的鏡像。 FROM scratch 會使用一個完全乾淨的檔案系統,不包含任何檔案。 因為Go語言編譯後不需要運行時,也就不需要安裝任何的運行庫。 FROM scratch 可以使得最後產生的鏡像最小化,其中只包含了 server 程式。

在 Docker 17.05版本以後,就有了新的解決方案,直接一個Dockerfile就可以解決:

# 編譯階段

FROM  golang:1.10.3

COPY  server.go /build/

WORKDIR  /build

RUN  CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOARM=6 go build -ldflags'-w -s'-o server

# 運行階段

FROM  scratch

# 從編譯階段的中拷貝編譯結果到當前鏡像中

COPY  --from=0 /build/server /

ENTRYPOINT  ["/server"]

這個 Dockerfile 的玄妙之處就在於 COPY 指令的 --from=0 參數,從前邊的階段中拷貝檔案到當前階段中,多個FROM語句時,0代表第一個階段。除了使用數字,我們還可以給階段命名,比如:

# 編譯階段 命名為 builder

FROM golang:1.10.3as builder

# ... 省略

# 運行階段

FROM scratch

# 從編譯階段的中拷貝編譯結果到當前鏡像中

COPY  --from=builder /build/server /

更為強大的是,COPY --from 不但可以從前置階段中拷貝,還可以直接從一個已經存在的鏡像中拷貝。比如,

FROM  ubuntu:16.04

COPY  --from=quay.io/coreos/etcd:v3.3.9 /usr/local/bin/etcd /usr/local/bin/

我們直接將etcd鏡像中的程式拷貝到了我們的鏡像中,這樣,在產生我們的程式鏡像時,就不需要源碼編譯etcd了,直接將官方編譯好的程式檔案拿過來就行了。

有些程式要麼沒有apt源,要麼apt源中的版本太老,要麼乾脆只提供源碼需要自己編譯,使用這些程式時,我們可以方便地使用已經存在的Docker鏡像作為我們的基礎鏡像。但是我們的軟體有時候可能需要依賴多個這種檔案,我們並不能同時將 nginx 和 etcd 的鏡像同時作為我們的基礎鏡像(不支援多根),這種情況下,使用 COPY --from 就非常方便實用了。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.