Docker 的核心思想就是如何將應用整合到容器中,而且能在容器中實際運行。
將應用整合到容器中而且運行起來的這個過程,稱爲「容器化」(Containerizing),有時也叫做「Docker化」(Dockerizing)。
容器是爲應用而生的,具體來講,容器可以簡化應用的構建、部署和運行過程。
完整的應用容器化過程主要分爲如下幾個步驟。java
- 編寫應用代碼。
- 建立一個 Dockerfile,其中包括當前應用的描述、依賴以及該如何運行這個應用。
- 對該 Dockerfile 執行 docker image build 命令。
- 等待 Docker 將應用程序構建到 Docker 鏡像中。
一旦應用容器化完成(即應用被打包爲一個 Docker 鏡像),就能以鏡像的形式交付並以容器的方式運行了。
下圖展現了上述步驟。
node

單體應用容器化
接下來咱們會逐步展現如何將一個簡單的單節點 Node.js Web 應用容器化。
若是是 Windows 操做系統的話,處理過程也是大同小異。
應用容器化的過程大體分爲以下幾個步驟:react
- 獲取應用代碼。
- 分析 Dockerfile。
- 構建應用鏡像。
- 運行該應用。
- 測試應用。
- 容器應用化細節。
- 生產環境中的多階段構建。
- 最佳實踐。
1) 獲取應用代碼
應用代碼能夠從網盤獲取(https://pan.baidu.com/s/150UgIJPvuQUf0yO3KBLegg 提取碼:pkx4)。linux
$ cd psweb
$ ls -l
total 28
-rw-r--r-- 1 root root 341 Sep 29 16:26 app.js
-rw-r--r-- 1 root root 216 Sep 29 16:26 circle.yml
-rw-r--r-- 1 root root 338 Sep 29 16:26 Dockerfile
-rw-r--r-- 1 root root 421 Sep 29 16:26 package.json
-rw-r--r-- 1 root root 370 Sep 29 16:26 README.md
drwxr-xr-x 2 root root 4096 Sep 29 16:26 test
drwxr-xr-x 2 root root 4096 Sep 29 16:26 viewsweb
該目錄下包含了所有的應用源碼,以及包含界面和單元測試的子目錄。這個應用結構很是簡單。
應用代碼準備就緒後,接下來分析一下 Dockerfile 的具體內容。spring
2) 分析 Dockerfile
在代碼目錄當中,有個名稱爲 Dockerfile 的文件。這個文件包含了對當前應用的描述,而且能指導 Docker 完成鏡像的構建。
在 Docker 當中,包含應用文件的目錄一般被稱爲構建上下文(Build Context)。一般將 Dockerfile 放到構建上下文的根目錄下。
另外很重要的一點是,文件開頭字母是大寫 D,這裏是一個單詞。像「dockerfile」或者「Docker file」這種寫法都是不容許的。
接下來了解一下 Dockerfile 文件當中都包含哪些具體內容。docker
$ cat Dockerfile
FROM alpine
LABEL maintainer="nigelpoulton@hotmail.com"
RUN apk add --update nodejs nodejs-npm
COPY . /src
WORKDIR /src
RUN npm install
EXPOSE 8080
ENTRYPOINT ["node", "./app.js"]npm
Dockerfile 主要包括兩個用途:json
- 對當前應用的描述。
- 指導 Docker 完成應用的容器化(建立一個包含當前應用的鏡像)。
不要因 Dockerfile 就是一個描述文件而對其有所輕視!Dockerfile 能實現開發和部署兩個過程的無縫切換。
同時 Dockerfile 還能幫助新手快速熟悉這個項目。Dockerfile 對當前的應用及其依賴有一個清晰準確的描述,而且很是容易閱讀和理解。
所以,要像重視你的代碼同樣重視這個文件,而且將它歸入到源控制系統當中。
下面是這個文件中的一些關鍵步驟概述:以 alpine 鏡像做爲當前鏡像基礎,指定維護者(maintainer)爲「nigelpoultion@hotmail.com」,安裝 Node.js 和 NPM,將應用的代碼複製到鏡像當中,設置新的工做目錄,安裝依賴包,記錄應用的網絡端口,最後將 app.js 設置爲默認運行的應用。
具體分析一下每一步的做用。
每一個 Dockerfile 文件第一行都是 FROM 指令。
FROM 指令指定的鏡像,會做爲當前鏡像的一個基礎鏡像層,當前應用的剩餘內容會做爲新增鏡像層添加到基礎鏡像層之上。
本例中的應用基於 Linux 操做系統,因此在 FROM 指令當中所引用的也是一個 Linux 基礎鏡像;若是要容器化的應用是一個基於 Windows 操做系統的應用,就須要指定一個像 microsoft/aspnetcore-build 這樣的 Windows 基礎鏡像了。
截至目前,基礎鏡像的結構以下圖所示。
瀏覽器

接下來,Dockerfile 中經過標籤(LABLE)方式指定了當前鏡像的維護者爲「nigelpoulton@hotmail. com」。
每一個標籤實際上是一個鍵值對(Key-Value),在一個鏡像當中能夠經過增長標籤的方式來爲鏡像添加自定義元數據。
備註維護者信息有助於爲該鏡像的潛在使用者提供溝通途徑,這是一種值得提倡的作法。RUN apk add --update nodejs nodejs-npm
指令使用 alpine 的 apk 包管理器將 nodejs 和 nodejs-npm 安裝到當前鏡像之中。
RUN 指令會在 FROM 指定的 alpine 基礎鏡像之上,新建一個鏡像層來存儲這些安裝內容。當前鏡像的結構以下圖所示。

COPY. / src 指令將應用相關文件從構建上下文複製到了當前鏡像中,而且新建一個鏡像層來存儲。COPY 執行結束以後,當前鏡像共包含 3 層,以下圖所示。

下一步,Dockerfile 經過 WORKDIR 指令,爲 Dockerfile 中還沒有執行的指令設置工做目錄。
該目錄與鏡像相關,而且會做爲元數據記錄到鏡像配置中,但不會建立新的鏡像層。
而後,RUN npm install
指令會根據 package.json 中的配置信息,使用 npm 來安裝當前應用的相關依賴包。
npm 命令會在前文設置的工做目錄中執行,而且在鏡像中新建鏡像層來保存相應的依賴文件。
目前鏡像一共包含 4 層,以下圖所示。

由於當前應用須要經過 TCP 端口 8080 對外提供一個 Web 服務,因此在 Dockerfile 中經過 EXPOSE 8080 指令來完成相應端口的設置。
這個配置信息會做爲鏡像的元數據被保存下來,並不會產生新的鏡像層。
最終,經過 ENTRYPOINT 指令來指定當前鏡像的入口程序。ENTRYPOINT 指定的配置信息也是經過鏡像元數據的形式保存下來,而不是新增鏡像層。
3) 容器化當前應用/構建具體的鏡像
到目前爲止,應該已經瞭解基本的原理和流程,接下來是時候嘗試構建本身的鏡像了。
下面的命令會構建並生成一個名爲 web:latest 的鏡像。命令最後的點(.)表示 Docker 在進行構建的時候,使用當前目錄做爲構建上下文。
必定要在命令最後包含這個點,而且在執行命令前,要確認當前目錄是 psweb(包含 Dockerfile 和應用代碼的目錄)。
命令執行結束後,檢查本地 Docker 鏡像庫是否包含了剛纔構建的鏡像。
$ docker image ls
REPO TAG IMAGE ID CREATED SIZE
web latest fc69fdc4c18e 10 seconds ago 64.4MB
恭喜,應用容器化已經成功了!
讀者能夠經過 docker image inspect web:latest
來確認剛剛構建的鏡像配置是否正確。這個命令會列出 Dockerfile 中設置的全部配置項。
4) 推送鏡像到倉庫
在建立一個鏡像以後,將其保存在一個鏡像倉庫服務是一個不錯的方式。這樣存儲鏡像會比較安全,而且能夠被其餘人訪問使用。
Docker Hub 就是這樣的一個開放的公共鏡像倉庫服務,而且這也是docker image push
命令默認的推送地址。
在推送鏡像以前,須要先使用 Docker ID 登陸 Docker Hub。除此以外,還須要爲待推送的鏡像打上合適的標籤。
接下來介紹一下如何登陸 Docker Hub,並將鏡像推送到其中。
在後續的例子中,須要用本身的 Docker ID 替換示例中所使用的 ID。因此每當看到「nigelpoulton」時,記得替換爲本身的 Docker ID。
$ docker login
Login with **your** Docker ID to push and pull images from Docker Hub...
Username: nigelpoulton
Password:
Login Succeeded
推送 Docker 鏡像以前,還須要爲鏡像打標籤。這是由於 Docker 在鏡像推送的過程當中須要以下信息。
- Registry(鏡像倉庫服務)。
- Repository(鏡像倉庫)。
- Tag(鏡像標籤)。
無須爲 Registry 和 Tag 指定值。當沒有爲上述信息指定具體值的時候,Docker 會默認 Registry=docker.io、Tag=latest。
可是 Docker 並無給 Repository 提供默認值,而是從被推送鏡像中的 REPOSITORY 屬性值獲取。
這一點可能很差理解,下面會經過一個完整的例子來介紹如何向 Docker Hub 中推送一個鏡像。
在前面的例子中執行了 docker image ls
命令。在該命令對應的輸出內容中能夠看到,鏡像倉庫的名稱是 web。
這意味着執行 docker image push
命令,會嘗試將鏡像推送到 docker.io/web:latest 中。
可是其實 nigelpoulton 這個用戶並無 web 這個鏡像倉庫的訪問權限,因此只能嘗試推送到 nigelpoulton 這個二級命名空間(Namespace)之下。
所以須要使用 nigelpoulton 這個 ID,爲當前鏡像從新打一個標籤。
$ docker image tag web:latest nigelpoulton/web:latest
爲鏡像打標籤命令的格式是docker image tag <current-tag> <new-tag>,其做用是爲指定的鏡像添加一個額外的標籤,而且不須要覆蓋已經存在的標籤。
再次執行 docker image ls
命令,能夠看到這個鏡像如今有了兩個標籤,其中一個包含 Docker ID nigelpoulton。
$ docker image ls
REPO TAG IMAGE ID CREATED SIZE
web latest fc69fdc4c18e 10 secs ago 64.4MB
nigelpoulton/web latest fc69fdc4c18e 10 secs ago 64.4MB
如今將該鏡像推送到 Docker Hub。
$ docker image push nigelpoulton/web:latest
The push refers to repository [docker.io/nigelpoulton/web]
2444b4ec39ad: Pushed
ed8142d2affb: Pushed
d77e2754766d: Pushed
cd7100a72410: Mounted from library/alpine
latest: digest: sha256:68c2dea730...f8cf7478 size: 1160
下圖展現了 Docker 如何肯定鏡像所要推送的目的倉庫。

由於權限問題,因此須要把上面例子中出現的 ID(nigelpoulton)替換爲本身的 Docker ID,才能進行推送操做。
在接下來的例子當中,將使用 web:latest 這個標籤。
5) 運行應用程序
前文中容器化的這個應用程序其實很簡單,從 app.js 這個文件內容中能夠看出,這其實就是一個在 8080 端口提供 Web 服務的應用程序。
下面的命令會基於 web:latest 這個鏡像,啓動一個名爲 c1 的容器。該容器將內部的 8080 端口與 Docker 主機的 80 端口進行映射。
這意味讀者能夠打開一個瀏覽器,在地址欄輸入 Docker 主機的 DNS 名稱或者 IP 地址,而後就能直接訪問這個 Web 應用了。
若是 Docker 主機已經運行了某個使用 80 端口的應用程序,讀者能夠在執行 docker container run
命令時指定一個不一樣的映射端口。例如,可使用 -p 5000:8080 參數,將 Docker 內部應用程序的 8080 端口映射到主機的 5000 端口。
$ docker container run -d --name c1 \
-p 80:8080 \
web:latest
-d 參數的做用是讓應用程序以守護線程的方式在後臺運行。
-p 80:8080 參數的做用是將主機的80端口與容器內的8080端口進行映射。
接下來驗證一下程序是否真的成功運行,而且對外提供服務的端口是否正常工做。
$ docker container ls
ID IMAGE COMMAND STATUS PORTS
49.. web:latest "node ./app.js" UP 6 secs 0.0.0.0:80->8080/tcp
爲了方便閱讀,只截取了命令輸出內容的一部分。從上面的輸出內容中能夠看到,容器已經正常運行。須要注意的是,80端口已經成功映射到了 8080 之上,而且任意外部主機(0.0.0.0:80)都可以經過 80 端口訪問該容器。
6) APP測試
打開瀏覽器,在地址欄輸入 DNS 名稱或者 IP 地址,就能訪問到正在運行的應用程序了。能夠看到下圖所示的界面。

若是沒有出現這樣的界面,嘗試執行下面的檢查來確認緣由所在。
使用 docker container ls
指令來確認容器已經啓動而且正常運行。容器名稱是c1,而且從輸出內容中能看到 0.0.0.0:80->8080/tcp。
確認防火牆或者其餘網絡安全設置沒有阻止訪問 Docker 主機的 80 端口。
如此,應用程序已經容器化併成功運行了。
7) 詳述
到如今爲止,應當成功完成一個示例應用程序的容器化。下面是其中一些細節部分的回顧和總結。
Dockerfile 中的註釋行,都是以#開頭的。
除註釋以外,每一行都是一條指令(Instruction)。指令的格式是指令參數以下。
INSTRUCTION argument
指令是不區分大小寫的,可是一般都採用大寫的方式。這樣 Dockerfile 的可讀性會高一些。Docker image build
命令會按行來解析 Dockerfile 中的指令並順序執行。
部分指令會在鏡像中建立新的鏡像層,其餘指令只會增長或修改鏡像的元數據信息。
在上面的例子當中,新增鏡像層的指令包括 FROM、RUN 以及 COPY,而新增元數據的指令包括 EXPOSE、WORKDIR、ENV以 及 ENTERPOINT。
關於如何區分命令是否會新建鏡像層,一個基本的原則是,若是指令的做用是向鏡像中增添新的文件或者程序,那麼這條指令就會新建鏡像層;若是隻是告訴 Docker 如何完成構建或者如何運行應用程序,那麼就只會增長鏡像的元數據。
能夠經過docker image history
來查看在構建鏡像的過程當中都執行了哪些指令。
在上面的輸出內容當中,有兩點是須要注意的。
首先,每行內容都對應了 Dockerfile 中的一條指令(順序是自下而上)。CREATE BY 這一列中還展現了當前行具體對應 Dockerfile 中的哪條指令。
其次,從這個輸出內容中,能夠觀察到只有 4 條指令會新建鏡像層(就是那些 SIZE 列對應的數值不爲零的指令),分別對應 Dockerfile 中的 FROM、RUN 以及 COPY 指令。
雖然其餘指令看上去跟這些新建鏡像層的指令並沒有區別,但實際上它們只在鏡像中新增了元數據信息。這些指令之因此看起來沒有區別,是由於 Docker 對以前構建鏡像層方式的兼容。
能夠經過執行 docker image inspect
指令來確認確實只有 4 個層被建立了。
$ docker image inspect web:latest
<Snip>
},
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:cd7100...1882bd56d263e02b6215",
"sha256:b3f88e...cae0e290980576e24885",
"sha256:3cfa21...cc819ef5e3246ec4fe16",
"sha256:4408b4...d52c731ba0b205392567"
]
},
使用 FROM 指令引用官方基礎鏡像是一個很好的習慣,這是由於官方的鏡像一般會遵循一些最佳實踐,而且能幫助使用者規避一些已知的問題。
除此以外,使用 FROM 的時候選擇一個相對較小的鏡像文件一般也能避免一些潛在的問題。
經過 docker image build
命令具體的輸出內容,能夠了解鏡像構建的過程。
在下面的片斷中,能夠看到基本的構建過程是,運行臨時容器 -> 在該容器中運行 Dockerfile 中的指令 -> 將指令運行結果保存爲一個新的鏡像層 -> 刪除臨時容器。
Step 3/8 : RUN apk add --update nodejs nodejs-npm
---> Running in e690ddca785f << Run inside of temp container
fetch http://dl-cdn...APKINDEX.tar.gz
fetch http://dl-cdn...APKINDEX.tar.gz
(1/10) Installing ca-certificates (20171114-r0)
<Snip>
OK: 61 MiB in 21 packages
---> c1d31d36b81f << Create new layer
Removing intermediate container << Remove temp container
Step 4/8 : COPY . /src
生產環境中的多階段構建
對於 Docker 鏡像來講,過大的體積並很差!
越大則越慢,這就意味着更難使用,並且可能更加脆弱,更容易遭受攻擊。
鑑於此,Docker 鏡像應該儘可能小。對於生產環境鏡像來講,目標是將其縮小到僅包含運行應用所必需的內容便可。問題在於,生成較小的鏡像並不是易事。
不一樣的 Dockerfile 寫法就會對鏡像的大小產生顯著影響。
常見的例子是,每個 RUN 指令會新增一個鏡像層。所以,經過使用 && 鏈接多個命令以及使用反斜槓(\)換行的方法,將多個命令包含在一個 RUN 指令中,一般來講是一種值得提倡的方式。
另外一個問題是開發者一般不會在構建完成後進行清理。當使用 RUN 執行一個命令時,可能會拉取一些構建工具,這些工具會留在鏡像中移交至生產環境。
有多種方式來改善這一問題——好比常見的是採用建造者模式(Builder Pattern)。但不管採用哪一種方式,一般都須要額外的培訓,而且會增長構建的複雜度。
建造者模式須要至少兩個 Dockerfile,一個用於開發環境,一個用於生產環境。
首先須要編寫 Dockerfile.dev,它基於一個大型基礎鏡像(Base Image),拉取所需的構建工具,並構建應用。
接下來,須要基於 Dockerfile.dev 構建一個鏡像,並用這個鏡像建立一個容器。
這時再編寫 Dockerfile.prod,它基於一個較小的基礎鏡像開始構建,並從剛纔建立的容器中將應用程序相關的部分複製過來。
整個過程須要編寫額外的腳本才能串聯起來。
這種方式是可行的,可是比較複雜。
多階段構建(Multi-Stage Build)是一種更好的方式!
多階段構建可以在不增長複雜性的狀況下優化構建過程。
下面介紹一下多階段構建方式。
多階段構建方式使用一個 Dockerfile,其中包含多個 FROM 指令。每個 FROM 指令都是一個新的構建階段(Build Stage),而且能夠方便地複製以前階段的構件。
示例源碼可從百度網盤獲取(https://pan.baidu.com/s/1M2paPY0f0lE5wm48HBk-Zw 提取碼: 2e7s ),Dockerfile 位於app目錄。
這是一個基於 Linux 系統的應用,所以只能運行在 Linux 容器環境上。
Dockerfile 以下所示。
FROM node:latest AS storefront
WORKDIR /usr/src/atsea/app/react-app
COPY react-app .
RUN npm install
RUN npm run build
FROM maven:latest AS appserver
WORKDIR /usr/src/atsea
COPY pom.xml .
RUN mvn -B -f pom.xml -s /usr/share/maven/ref/settings-docker.xml dependency
\:resolve
COPY . .
RUN mvn -B -s /usr/share/maven/ref/settings-docker.xml package -DskipTests
FROM java:8-jdk-alpine AS production
RUN adduser -Dh /home/gordon gordon
WORKDIR /static
COPY --from=storefront /usr/src/atsea/app/react-app/build/ .
WORKDIR /app
COPY --from=appserver /usr/src/atsea/target/AtSea-0.0.1-SNAPSHOT.jar .
ENTRYPOINT ["java", "-jar", "/app/AtSea-0.0.1-SNAPSHOT.jar"]
CMD ["--spring.profiles.active=postgres"]
首先注意到,Dockerfile 中有 3 個 FROM 指令。每個 FROM 指令構成一個單獨的構建階段。
各個階段在內部從 0 開始編號。不過,示例中針對每一個階段都定義了便於理解的名字。
- 階段 0 叫做 storefront。
- 階段 1 叫做 appserver。
- 階段 2 叫做 production。
storefront 階段拉取了大小超過 600MB 的 node:latest 鏡像,而後設置了工做目錄,複製一些應用代碼進去,而後使用 2 個 RUN 指令來執行 npm 操做。
這會生成 3 個鏡像層並顯著增長鏡像大小。指令執行結束後會獲得一個比原鏡像大得多的鏡像,其中包含許多構建工具和少許應用程序代碼。
appserver 階段拉取了大小超過 700MB 的 maven:latest 鏡像。而後經過 2 個 COPY 指令和 2 個 RUN 指令生成了 4 個鏡像層。
這個階段一樣會構建出一個很是大的包含許多構建工具和很是少許應用程序代碼的鏡像。
production 階段拉取 java:8-jdk-alpine 鏡像,這個鏡像大約 150MB,明顯小於前兩個構建階段用到的 node 和 maven 鏡像。
這個階段會建立一個用戶,設置工做目錄,從 storefront 階段生成的鏡像中複製一些應用代碼過來。
以後,設置一個不一樣的工做目錄,而後從 appserver 階段生成的鏡像中複製應用相關的代碼。最後,production 設置當前應用程序爲容器啓動時的主程序。
重點在於 COPY --from 指令,它從以前的階段構建的鏡像中僅複製生產環境相關的應用代碼,而不會複製生產環境不須要的構件。
還有一點也很重要,多階段構建這種方式僅用到了一個 Dockerfile,而且 docker image build
命令不須要增長額外參數。
下面演示一下構建操做。克隆代碼庫並切換到 app 目錄,並確保其中有 Dockerfile。
$ cd atsea-sample-shop-app/app
$ ls -l
total 24
-rw-r--r-- 1 root root 682 Oct 1 22:03 Dockerfile
-rw-r--r-- 1 root root 4365 Oct 1 22:03 pom.xml
drwxr-xr-x 4 root root 4096 Oct 1 22:03 react-app
drwxr-xr-x 4 root root 4096 Oct 1 22:03 src
執行構建(這可能會花費幾分鐘)。
$ docker image build -t multi:stage .
Sending build context to Docker daemon 3.658MB
Step 1/19 : FROM node:latest AS storefront
latest: Pulling from library/node
aa18ad1a0d33: Pull complete
15a33158a136: Pull complete
<Snip>
Step 19/19 : CMD --spring.profiles.active=postgres
---> Running in b4df9850f7ed
---> 3dc0d5e6223e
Removing intermediate container b4df9850f7ed
Successfully built 3dc0d5e6223e
Successfully tagged multi:stage
示例中 multi:stage 標籤是自行定義的,能夠根據本身的須要和規範來指定標籤名稱。不過並不要求必定必須爲多階段構建指定標籤。
執行 docker image ls
命令查看由構建命令拉取和生成的鏡像。
$ docker image ls
REPO TAG IMAGE ID CREATED SIZE
node latest 9ea1c3e33a0b 4 days ago 673MB
<none> <none> 6598db3cefaf 3 mins ago 816MB
maven latest cbf114925530 2 weeks ago 750MB
<none> <none> d5b619b83d9e 1 min ago 891MB
java 8-jdk-alpine 3fd9dd82815c 7 months ago 145MB
multi stage 3dc0d5e6223e 1 min ago 210MB
輸出內容的第一行顯示了在 storefront 階段拉取的 node:latest 鏡像,下一行內容爲該階段生成的鏡像(經過添加代碼,執行 npm 安裝和構建操做生成該鏡像)。
這兩個都包含許多的構建工具,所以鏡像體積很是大。
第 3~4 行是在 appserver 階段拉取和生成的鏡像,它們也都由於包含許多構建工具而致使體積較大。
最後一行是 Dockerfile 中的最後一個構建階段(stage2/production)生成的 multi:stage 鏡像。
可見它明顯比以前階段拉取和生成的鏡像要小。這是由於該鏡像是基於相對精簡的 java:8-jdk-alpine 鏡像構建的,而且僅添加了用於生產環境的應用程序文件。
最終,無須額外的腳本,僅對一個單獨的 Dockerfile 執行 docker image build
命令,就建立了一個精簡的生產環境鏡像。
多階段構建是隨 Docker 17.05 版本新增的一個特性,用於構建精簡的生產環境鏡像。
最佳實踐
下面介紹一些最佳實踐。
1) 利用構建緩存
Docker 的構建過程利用了緩存機制。觀察緩存效果的一個方法,就是在一個乾淨的 Docker 主機上構建一個新的鏡像,而後再重複一樣的構建。
第一次構建會拉取基礎鏡像,並構建鏡像層,構建過程須要花費必定時間;第二次構建幾乎可以當即完成。
這就是由於第一次構建的內容(如鏡像層)可以被緩存下來,並被後續的構建過程複用。docker image build
命令會從頂層開始解析 Dockerfile 中的指令並逐行執行。而對每一條指令,Docker 都會檢查緩存中是否已經有與該指令對應的鏡像層。
若是有,即爲緩存命中(Cache Hit),而且會使用這個鏡像層;若是沒有,則是緩存未命中(Cache Miss),Docker 會基於該指令構建新的鏡像層。
緩存命中可以顯著加快構建過程。
下面經過實例演示其效果。
示例用的 Dockerfile 以下。
FROM alpine
RUN apk add --update nodejs nodejs-npm
COPY . /src
WORKDIR /src
RUN npm install
EXPOSE 8080
ENTRYPOINT ["node", "./app.js"]
第一條指令告訴 Docker 使用 alpine:latest 做爲基礎鏡像。
若是主機中已經存在這個鏡像,那麼構建時會直接跳到下一條指令;若是鏡像不存在,則會從 Docker Hub(docker.io)拉取。
下一條指令(RUN apk...)對鏡像執行一條命令。
此時,Docker 會檢查構建緩存中是否存在基於同一基礎鏡像,而且執行了相同指令的鏡像層。
在此例中,Docker 會檢查緩存中是否存在一個基於 alpine:latest 鏡像且執行了 RUN apk add --update nodejs nodejs-npm
指令構建獲得的鏡像層。
若是找到該鏡像層,Docker 會跳過這條指令,並連接到這個已經存在的鏡像層,而後繼續構建;若是沒法找到符合要求的鏡像層,則設置緩存無效並構建該鏡像層。
此處「設置緩存無效」做用於本次構建的後續部分。也就是說 Dockerfile 中接下來的指令將所有執行而不會再嘗試查找構建緩存。
假設 Docker 已經在緩存中找到了該指令對應的鏡像層(緩存命中),而且假設這個鏡像層的 ID 是 AAA。
下一條指令會複製一些代碼到鏡像中(COPY . /src)。由於上一條指令命中了緩存,Docker 會繼續查找是否有一個緩存的鏡像層也是基於 AAA 層並執行了 COPY . /src 命令。
若是有,Docker 會連接到這個緩存的鏡像層並繼續執行後續指令;若是沒有,則構建鏡像層,並對後續的構建操做設置緩存無效。
假設 Docker 已經有一個對應該指令的緩存鏡像層(緩存命中),而且假設這個鏡像層的 ID 是 BBB。
那麼 Docker 將繼續執行 Dockerfile 中剩餘的指令。
理解如下幾點很重要。
首先,一旦有指令在緩存中未命中(沒有該指令對應的鏡像層),則後續的整個構建過程將再也不使用緩存。
在編寫 Dockerfile 時須特別注意這一點,儘可能將易於發生變化的指令置於 Dockerfile 文件的後方執行。
這意味着緩存未命中的狀況將直到構建的後期纔會出現,從而構建過程可以儘可能從緩存中獲益。
經過對 docker image build
命令加入 --nocache=true 參數能夠強制忽略對緩存的使用。
還有一點也很重要,那就是 COPY 和 ADD 指令會檢查複製到鏡像中的內容自上一次構建以後是否發生了變化。
例如,有可能 Dockerfile 中的 COPY . /src 指令沒有發生變化,可是被複制的目錄中的內容已經發生變化了。
爲了應對這一問題,Docker 會計算每個被複制文件的 Checksum 值,並與緩存鏡像層中同一文件的 checksum 進行對比。若是不匹配,那麼就認爲緩存無效並構建新的鏡像層。
2) 合併鏡像
合併鏡像並不是一個最佳實踐,由於這種方式利弊參半。
整體來講,Docker 會遵循正常的方式構建鏡像,但以後會增長一個額外的步驟,將全部的內容合併到一個鏡像層中。
當鏡像中層數太多時,合併是一個不錯的優化方式。例如,當建立一個新的基礎鏡像,以便基於它來構建其餘鏡像的時候,這個基礎鏡像就最好被合併爲一層。
缺點是,合併的鏡像將沒法共享鏡像層。這會致使存儲空間的低效利用,並且 push 和 pull 操做的鏡像體積更大。
執行 docker image build
命令時,能夠經過增長 --squash 參數來建立一個合併的鏡像。
下圖闡釋了合併鏡像層帶來的存儲空間低效利用的問題。

兩個鏡像的內容是徹底同樣的,區別在因而否進行了合併。在使用 docker image push
命令發送鏡像到 Docker Hub 時,合併的鏡像須要發送所有字節,而不合並的鏡像只須要發送不一樣的鏡像層便可。
3) 使用 no-install-recommends
在構建 Linux 鏡像時,若使用的是 APT 包管理器,則應該在執行 apt-get install 命令時增長 no-install-recommends 參數。
這可以確保 APT 僅安裝核心依賴(Depends 中定義)包,而不是推薦和建議的包。這樣可以顯著減小沒必要要包的下載數量。
4) 不要安裝 MSI 包(Windows)
在構建 Windows 鏡像時,儘可能避免使用 MSI 包管理器。因其對空間的利用率不高,會大幅增長鏡像的體積。