Docker 17.05版本之後,新增了Dockerfile多階段構建。所謂多階段構建,其實是容許一個Dockerfile 中出現多個 FROM
指令。這樣作有什麼意義呢?html
在17.05版本以前的Docker,只容許Dockerfile中出現一個FROM
指令,這得從鏡像的本質提及。linux
在《Docker概念簡介》 中咱們提到,你能夠簡單理解Docker的鏡像是一個壓縮文件,其中包含了你須要的程序和一個文件系統。其實這樣說是不嚴謹的,Docker鏡像並不是只是一個文件,而是由一堆文件組成,最主要的文件是 層。nginx
Dockerfile 中,大多數指令會生成一個層,好比下方的兩個例子:git
# 示例一,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層。golang
若是ubuntu:16.04
等其餘鏡像不算,若是系統中只存在 foo 和 bar 兩個鏡像,那麼系統中一共保存了多少層呢?ubuntu
是7層,並不是13層,這是由於,foo和bar共享了6層。層的共享機制能夠節約大量的磁盤空間和傳輸帶寬,好比你本地已經有了foo鏡像,又從鏡像倉庫中拉取bar鏡像時,只拉取本地所沒有的最後一層就能夠了,不須要把整個bar鏡像連根拉一遍。可是層共享是怎樣實現的呢?工具
原來,Docker鏡像的每一層只記錄文件變動,在容器啓動時,Docker會將鏡像的各個層進行計算,最後生成一個文件系統,這個被稱爲 聯合掛載。對此感興趣的話能夠進入瞭解一下 AUFS。ui
Docker的各個層是有相關性的,在聯合掛載的過程當中,系統須要知道在什麼樣的基礎上再增長新的文件。那麼這就要求一個Docker鏡像只能有一個起始層,只能有一個根。因此,Dockerfile中,就只容許一個FROM
指令。由於多個FROM
指令會形成多根,則是沒法實現的。但爲何 Docker 17.05 版本之後容許 Dockerfile支持多個 FROM
指令了呢,莫非已經支持了多根?code
多個 FROM 指令並非爲了生成多根的層關係,最後生成的鏡像,仍以最後一條 FROM 爲準,以前的 FROM 會被拋棄,那麼以前的FROM 又有什麼意義呢?server
每一條 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.3 as 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
就很是方便實用了。