映象的快取特性
Docker 會快取已有映象的映象層,構建新映象時,如果某映象層已經存在,就直接使用,無需重新建立。
舉例說明。
在前面的 Dockerfile 中新增一點新內容,往映象中複製一個檔案:
① 確保 testfile 已存在。
② 重點在這裡:之前已經執行過相同的 RUN 指令,這次直接使用快取中的映象層 35ca89798937。
③ 執行 COPY 指令。
其過程是啟動臨時容器,複製 testfile,提交新的映象層 8d02784a78f4,刪除臨時容器。
在 ubuntu-with-vi-dockerfile 映象上直接新增一層就得到了新的映象 ubuntu-with-vi-dockerfile-2。
如果我們希望在構建映象時不使用快取,可以在 docker build
命令中加上 --no-cache
引數。
Dockerfile 中每一個指令都會建立一個映象層,上層是依賴於下層的。無論什麼時候,只要某一層發生變化,其上面所有層的快取都會失效。
也就是說,如果我們改變 Dockerfile 指令的執行順序,或者修改或新增指令,都會使快取失效。
舉例說明,比如交換前面 RUN 和 COPY 的順序:
雖然在邏輯上這種改動對映象的內容沒有影響,但由於分層的結構特性,Docker 必須重建受影響的映象層。
從上面的輸出可以看到生成了新的映象層 bc87c9710f40,快取已經失效。
除了構建時使用快取,Docker 在下載映象時也會使用。例如我們下載 httpd 映象。
docker pull 命令輸出顯示第一層(base 映象)已經存在,不需要下載。
由 Dockerfile 可知 httpd 的 base 映象為 debian,正好之前已經下載過 debian 映象,所以有快取可用。通過 docker history 可以進一步驗證。
除錯 Dockerfile
包括 Dockerfile 在內的任何指令碼和程式都會出錯。有錯並不可怕,但必須有辦法排查,所以本節討論如何 debug Dockerfile。
先回顧一下通過 Dockerfile 構建映象的過程:
- 從 base 映象執行一個容器。
- 執行一條指令,對容器做修改。
- 執行類似 docker commit 的操作,生成一個新的映象層。
- Docker 再基於剛剛提交的映象執行一個新容器。
- 重複 2-4 步,直到 Dockerfile 中的所有指令執行完畢。
從這個過程可以看出,如果 Dockerfile 由於某種原因執行到某個指令失敗了,我們也將能夠得到前一個指令成功執行構建出的映象,這對除錯 Dockerfile 非常有幫助。我們可以執行最新的這個映象定位指令失敗的原因。
我們來看一個除錯的例子。Dockerfile 內容如下:
執行 docker build
:
Dockerfile 在執行第三步 RUN 指令時失敗。我們可以利用第二步建立的映象 22d31cc52b3e 進行除錯,方式是通過 docker run -it
啟動映象的一個容器。
手工執行 RUN 指令很容易定位失敗的原因是 busybox 映象中沒有 bash。雖然這是個極其簡單的例子,但它很好地展示了除錯 Dockerfile 的方法。
到這裡相信大家對 Dockerfile 的功能和使用流程有了比較完整的印象,但還沒有系統學習 Dockerfile 的各種指令和實際用法,下節會開始這個主題。
Dockerfile 常用指令
是時候系統學習 Dockerfile 了。
下面列出了 Dockerfile 中最常用的指令,完整列表和說明可參看官方文件。
FROM
指定 base 映象。
MAINTAINER
設定映象的作者,可以是任意字串。
COPY
將檔案從 build context 複製到映象。
COPY 支援兩種形式:
- COPY src dest
- COPY ["src", "dest"]
注意:src 只能指定 build context 中的檔案或目錄。
ADD
與 COPY 類似,從 build context 複製檔案到映象。不同的是,如果 src 是歸檔檔案(tar, zip, tgz, xz 等),檔案會被自動解壓到 dest。
ENV
設定環境變數,環境變數可被後面的指令使用。例如:
...
ENV MY_VERSION 1.3
RUN apt-get install -y mypackage=$MY_VERSION
...
EXPOSE
指定容器中的程式會監聽某個埠,Docker 可以將該埠暴露出來。我們會在容器網路部分詳細討論。
VOLUME
將檔案或目錄宣告為 volume。我們會在容器儲存部分詳細討論。
WORKDIR
為後面的 RUN, CMD, ENTRYPOINT, ADD 或 COPY 指令設定映象中的當前工作目錄。
RUN
在容器中執行指定的命令。
CMD
容器啟動時執行指定的命令。
Dockerfile 中可以有多個 CMD 指令,但只有最後一個生效。CMD 可以被 docker run 之後的引數替換。
ENTRYPOINT
設定容器啟動時執行的命令。
Dockerfile 中可以有多個 ENTRYPOINT 指令,但只有最後一個生效。CMD 或 docker run 之後的引數會被當做引數傳遞給 ENTRYPOINT。
下面我們來看一個較為全面的 Dockerfile:
注:Dockerfile 支援以“#”開頭的註釋。
構建映象:
① 構建前確保 build context 中存在需要的檔案。
② 依次執行 Dockerfile 指令,完成構建。
執行容器,驗證映象內容:
① 進入容器,當前目錄即為 WORKDIR。
如果 WORKDIR 不存在,Docker 會自動為我們建立。
② WORKDIR 中儲存了我們希望的檔案和目錄:
目錄 bunch:由 ADD 指令從 build context 複製的歸檔檔案 bunch.tar.gz,已經自動解壓。
檔案 tmpfile1:由 RUN 指令建立。
檔案 tmpfile2:由 COPY 指令從 build context 複製。
③ ENV 指令定義的環境變數已經生效。
在上面這些指令中,RUN、CMD、ENTRYPOINT 很重要且容易混淆,下節專門討論。
RUN vs CMD vs ENTRYPOINT
RUN、CMD 和 ENTRYPOINT 這三個 Dockerfile 指令看上去很類似很容易混淆。本節將通過實踐詳細討論它們的區別。
簡單的說
- RUN 執行命令並建立新的映象層RUN 經常用於安裝軟體包。
- CMD 設定容器啟動後預設執行的命令及其引數但 CMD 能夠被
docker run
後面跟的命令列引數替換。 - ENTRYPOINT 配置容器啟動時執行的命令。
下面我們詳細分析。
Shell 和 Exec 格式
我們可用兩種方式指定 RUN、CMD 和 ENTRYPOINT 要執行的命令Shell 格式和 Exec 格式二者在使用上有細微的區別。
例如
當指令執行時shell 格式底層會呼叫 /bin/sh -c <command> 。
例如下面的 Dockerfile 片段
執行 docker run <image> 將輸出
Hello, Cloud Man
複製程式碼
注意環境變數 name
已經被值 Cloud Man
替換。
下面來看 Exec 格式。
例如
當指令執行時會直接呼叫 <command>不會被 shell 解析。
例如下面的 Dockerfile 片段
執行容器將輸出
Hello, $name
複製程式碼
注意環境變數“name”沒有被替換。
如果希望使用環境變數照如下修改
執行容器將輸出
Hello, Cloud Man
複製程式碼
CMD 和 ENTRYPOINT 推薦使用 Exec 格式因為指令可讀性更強更容易理解。RUN 則兩種格式都可以。
RUN
RUN 指令通常用於安裝應用和軟體包。
RUN 在當前映象的頂部執行命令並通過建立新的映象層。Dockerfile 中常常包含多個 RUN 指令。
RUN 有兩種格式
- Shell 格式RUN
- Exec 格式RUN ["executable", "param1", "param2"]
下面是使用 RUN 安裝多個包的例子
注意apt-get update 和 apt-get install 被放在一個 RUN 指令中執行這樣能夠保證每次安裝的是最新的包。如果 apt-get install 在單獨的 RUN 中執行則會使用 apt-get update 建立的映象層而這一層可能是很久以前快取的。
CMD
CMD 指令允許使用者指定容器的預設執行的命令。
此命令會在容器啟動且 docker run 沒有指定其他命令時執行。
- 如果 docker run 指定了其他命令CMD 指定的預設命令將被忽略。
- 如果 Dockerfile 中有多個 CMD 指令只有最後一個 CMD 有效。
CMD 有三種格式
- Exec 格式CMD ["executable","param1","param2"]
這是 CMD 的推薦格式。 - CMD ["param1","param2"] 為 ENTRYPOINT 提供額外的引數此時 ENTRYPOINT 必須使用 Exec 格式。
- Shell 格式CMD command param1 param2
Exec 和 Shell 格式前面已經介紹過了。
第二種格式 CMD ["param1","param2"] 要與 Exec 格式 的 ENTRYPOINT 指令配合使用其用途是為 ENTRYPOINT 設定預設的引數。我們將在後面討論 ENTRYPOINT 時舉例說明。
下面看看 CMD 是如何工作的。Dockerfile 片段如下
CMD echo "Hello world"
複製程式碼
執行容器 docker run -it [image] 將輸出
Hello world
複製程式碼
但當後面加上一個命令比如 docker run -it [image] /bin/bashCMD 會被忽略掉命令 bash 將被執行
root@10a32dc7d3d3:/#
複製程式碼
ENTRYPOINT
ENTRYPOINT 指令可讓容器以應用程式或者服務的形式執行。
ENTRYPOINT 看上去與 CMD 很像它們都可以指定要執行的命令及其引數。不同的地方在於 ENTRYPOINT 不會被忽略一定會被執行即使執行 docker run 時指定了其他命令。
ENTRYPOINT 有兩種格式
- Exec 格式ENTRYPOINT ["executable", "param1", "param2"] 這是 ENTRYPOINT 的推薦格式。
- Shell 格式ENTRYPOINT command param1 param2
在為 ENTRYPOINT 選擇格式時必須小心因為這兩種格式的效果差別很大。
Exec 格式
ENTRYPOINT 的 Exec 格式用於設定要執行的命令及其引數同時可通過 CMD 提供額外的引數。
ENTRYPOINT 中的引數始終會被使用而 CMD 的額外引數可以在容器啟動時動態替換掉。
比如下面的 Dockerfile 片段
當容器通過 docker run -it [image] 啟動時輸出為
Hello world
複製程式碼
而如果通過 docker run -it [image] CloudMan 啟動則輸出為
Hello CloudMan
複製程式碼
Shell 格式
ENTRYPOINT 的 Shell 格式會忽略任何 CMD 或 docker run 提供的引數。
最佳實踐
- 使用 RUN 指令安裝應用和軟體包構建映象。
- 如果 Docker 映象的用途是執行應用程式或服務比如執行一個 MySQL應該優先使用 Exec 格式的 ENTRYPOINT 指令。CMD 可為 ENTRYPOINT 提供額外的預設引數同時可利用 docker run 命令列替換預設引數。
- 如果想為容器設定預設的啟動命令可使用 CMD 指令。使用者可在 docker run 命令列中替換此預設命令。
到這裡我們已經具備編寫 Dockerfile 的能力了。如果大家還覺得沒把握推薦一個快速掌握 Dockerfile 的方法去 Docker Hub 上參考那些官方映象的 Dockerfile。
好了我們已經學習完如何建立自己的 image下一節討論如何分發 image。
作者:cloudman6