天天看點

Dockerfile 指令詳解1

我們已經介紹了 <code>FROM</code>,<code>RUN</code>,還提及了 <code>COPY</code>, <code>ADD</code>,其實 <code>Dockerfile</code> 功能很強大,它提供了十多個指令。下面我們繼續講解其他的指令。

格式:

<code>COPY [--chown=&lt;user&gt;:&lt;group&gt;] &lt;源路徑&gt;... &lt;目标路徑&gt;</code>

<code>COPY [--chown=&lt;user&gt;:&lt;group&gt;] ["&lt;源路徑1&gt;",... "&lt;目标路徑&gt;"]</code>

和 <code>RUN</code> 指令一樣,也有兩種格式,一種類似于指令行,一種類似于函數調用。

<code>COPY</code> 指令将從建構上下文目錄中 <code>&lt;源路徑&gt;</code> 的檔案/目錄複制到新的一層的鏡像内的 <code>&lt;目标路徑&gt;</code> 位置。比如:

<code>&lt;源路徑&gt;</code> 可以是多個,甚至可以是通配符,其通配符規則要滿足 Go 的 <code>filepath.Match</code> 規則,如:

<code>&lt;目标路徑&gt;</code> 可以是容器内的絕對路徑,也可以是相對于工作目錄的相對路徑(工作目錄可以用 <code>WORKDIR</code> 指令來指定)。目标路徑不需要事先建立,如果目錄不存在會在複制檔案前先行建立缺失目錄。

此外,還需要注意一點,使用 <code>COPY</code> 指令,源檔案的各種中繼資料都會保留。比如讀、寫、執行權限、檔案變更時間等。這個特性對于鏡像定制很有用。特别是建構相關檔案都在使用 Git 進行管理的時候。

在使用該指令的時候還可以加上 <code>--chown=&lt;user&gt;:&lt;group&gt;</code> 選項來改變檔案的所屬使用者及所屬組。

<code>ADD</code> 指令和 <code>COPY</code> 的格式和性質基本一緻。但是在 <code>COPY</code> 基礎上增加了一些功能。

比如 <code>&lt;源路徑&gt;</code> 可以是一個 <code>URL</code>,這種情況下,Docker 引擎會試圖去下載下傳這個連結的檔案放到 <code>&lt;目标路徑&gt;</code> 去。下載下傳後的檔案權限自動設定為 <code>600</code>,如果這并不是想要的權限,那麼還需要增加額外的一層 <code>RUN</code> 進行權限調整,另外,如果下載下傳的是個壓縮包,需要解壓縮,也一樣還需要額外的一層 <code>RUN</code> 指令進行解壓縮。是以不如直接使用 <code>RUN</code> 指令,然後使用 <code>wget</code> 或者 <code>curl</code> 工具下載下傳,處理權限、解壓縮、然後清理無用檔案更合理。是以,這個功能其實并不實用,而且不推薦使用。

如果 <code>&lt;源路徑&gt;</code> 為一個 <code>tar</code> 壓縮檔案的話,壓縮格式為 <code>gzip</code>, <code>bzip2</code> 以及 <code>xz</code> 的情況下,<code>ADD</code> 指令将會自動解壓縮這個壓縮檔案到 <code>&lt;目标路徑&gt;</code> 去。

在某些情況下,這個自動解壓縮的功能非常有用,比如官方鏡像 <code>ubuntu</code> 中:

但在某些情況下,如果我們真的是希望複制個壓縮檔案進去,而不解壓縮,這時就不可以使用 <code>ADD</code> 指令了。

在 Docker 官方的 Dockerfile 最佳實踐文檔 中要求,盡可能的使用 <code>COPY</code>,因為 <code>COPY</code> 的語義很明确,就是複制檔案而已,而 <code>ADD</code> 則包含了更複雜的功能,其行為也不一定很清晰。最适合使用 <code>ADD</code> 的場合,就是所提及的需要自動解壓縮的場合。

另外需要注意的是,<code>ADD</code> 指令會令鏡像建構緩存失效,進而可能會令鏡像建構變得比較緩慢。

是以在 <code>COPY</code> 和 <code>ADD</code> 指令中選擇的時候,可以遵循這樣的原則,所有的檔案複制均使用 <code>COPY</code> 指令,僅在需要自動解壓縮的場合使用 <code>ADD</code>。

<code>CMD</code> 指令的格式和 <code>RUN</code> 相似,也是兩種格式:

<code>shell</code> 格式:<code>CMD &lt;指令&gt;</code>

<code>exec</code> 格式:<code>CMD ["可執行檔案", "參數1", "參數2"...]</code>

參數清單格式:<code>CMD ["參數1", "參數2"...]</code>。在指定了 <code>ENTRYPOINT</code> 指令後,用 <code>CMD</code> 指定具體的參數。

之前介紹容器的時候曾經說過,Docker 不是虛拟機,容器就是程序。既然是程序,那麼在啟動容器的時候,需要指定所運作的程式及參數。<code>CMD</code> 指令就是用于指定預設的容器主程序的啟動指令的。

在運作時可以指定新的指令來替代鏡像設定中的這個預設指令,比如,<code>ubuntu</code> 鏡像預設的 <code>CMD</code> 是 <code>/bin/bash</code>,如果我們直接 <code>docker run -it ubuntu</code> 的話,會直接進入 <code>bash</code>。我們也可以在運作時指定運作别的指令,如 <code>docker run -it ubuntu cat /etc/os-release</code>。這就是用 <code>cat /etc/os-release</code> 指令替換了預設的 <code>/bin/bash</code> 指令了,輸出了系統版本資訊。

在指令格式上,一般推薦使用 <code>exec</code> 格式,這類格式在解析時會被解析為 JSON 數組,是以一定要使用雙引号 <code>"</code>,而不要使用單引号。

如果使用 <code>shell</code> 格式的話,實際的指令會被包裝為 <code>sh -c</code> 的參數的形式進行執行。比如:

在實際執行中,會将其變更為:

這就是為什麼我們可以使用環境變量的原因,因為這些環境變量會被 shell 進行解析處理。

提到 <code>CMD</code> 就不得不提容器中應用在前台執行和背景執行的問題。這是初學者常出現的一個混淆。

Docker 不是虛拟機,容器中的應用都應該以前台執行,而不是像虛拟機、實體機裡面那樣,用 <code>systemd</code> 去啟動背景服務,容器内沒有背景服務的概念。

一些初學者将 <code>CMD</code> 寫為:

然後發現容器執行後就立即退出了。甚至在容器内去使用 <code>systemctl</code> 指令結果卻發現根本執行不了。這就是因為沒有搞明白前台、背景的概念,沒有區分容器和虛拟機的差異,依舊在以傳統虛拟機的角度去了解容器。

對于容器而言,其啟動程式就是容器應用程序,容器就是為了主程序而存在的,主程序退出,容器就失去了存在的意義,進而退出,其它輔助程序不是它需要關心的東西。

而使用 <code>service nginx start</code> 指令,則是希望 upstart 來以背景守護程序形式啟動 <code>nginx</code> 服務。而剛才說了 <code>CMD service nginx start</code> 會被了解為 <code>CMD [ "sh", "-c", "service nginx start"]</code>,是以主程序實際上是 <code>sh</code>。那麼當 <code>service nginx start</code> 指令結束後,<code>sh</code> 也就結束了,<code>sh</code> 作為主程序退出了,自然就會令容器退出。

正确的做法是直接執行 <code>nginx</code> 可執行檔案,并且要求以前台形式運作。比如:

<code>ENTRYPOINT</code> 的格式和 <code>RUN</code> 指令格式一樣,分為 <code>exec</code> 格式和 <code>shell</code> 格式。

<code>ENTRYPOINT</code> 的目的和 <code>CMD</code> 一樣,都是在指定容器啟動程式及參數。<code>ENTRYPOINT</code> 在運作時也可以替代,不過比 <code>CMD</code> 要略顯繁瑣,需要通過 <code>docker run</code> 的參數 <code>--entrypoint</code> 來指定。

當指定了 <code>ENTRYPOINT</code> 後,<code>CMD</code> 的含義就發生了改變,不再是直接的運作其指令,而是将 <code>CMD</code> 的内容作為參數傳給 <code>ENTRYPOINT</code> 指令,換句話說實際執行時,将變為:

那麼有了 <code>CMD</code> 後,為什麼還要有 <code>ENTRYPOINT</code> 呢?這種 <code>&lt;ENTRYPOINT&gt; "&lt;CMD&gt;"</code> 有什麼好處麼?讓我們來看幾個場景。

場景一:讓鏡像變成像指令一樣使用

假設我們需要一個得知自己目前公網 IP 的鏡像,那麼可以先用 <code>CMD</code> 來實作:

假如我們使用 <code>docker build -t myip .</code> 來建構鏡像的話,如果我們需要查詢目前公網 IP,隻需要執行:

嗯,這麼看起來好像可以直接把鏡像當做指令使用了,不過指令總有參數,如果我們希望加參數呢?比如從上面的 <code>CMD</code> 中可以看到實質的指令是 <code>curl</code>,那麼如果我們希望顯示 HTTP 頭資訊,就需要加上 <code>-i</code> 參數。那麼我們可以直接加 <code>-i</code> 參數給 <code>docker run myip</code> 麼?

我們可以看到可執行檔案找不到的報錯,<code>executable file not found</code>。之前我們說過,跟在鏡像名後面的是 <code>command</code>,運作時會替換 <code>CMD</code> 的預設值。是以這裡的 <code>-i</code> 替換了原來的 <code>CMD</code>,而不是添加在原來的 <code>curl -s https://ip.cn</code> 後面。而 <code>-i</code> 根本不是指令,是以自然找不到。

那麼如果我們希望加入 <code>-i</code> 這參數,我們就必須重新完整的輸入這個指令:

這顯然不是很好的解決方案,而使用 <code>ENTRYPOINT</code> 就可以解決這個問題。現在我們重新用 <code>ENTRYPOINT</code> 來實作這個鏡像:

這次我們再來嘗試直接使用 <code>docker run myip -i</code>:

可以看到,這次成功了。這是因為當存在 <code>ENTRYPOINT</code> 後,<code>CMD</code> 的内容将會作為參數傳給 <code>ENTRYPOINT</code>,而這裡 <code>-i</code> 就是新的 <code>CMD</code>,是以會作為參數傳給 <code>curl</code>,進而達到了我們預期的效果。

場景二:應用運作前的準備工作

啟動容器就是啟動主程序,但有些時候,啟動主程序前,需要一些準備工作。

比如 <code>mysql</code> 類的資料庫,可能需要一些資料庫配置、初始化的工作,這些工作要在最終的 mysql 伺服器運作之前解決。

此外,可能希望避免使用 <code>root</code> 使用者去啟動服務,進而提高安全性,而在啟動服務前還需要以 <code>root</code> 身份執行一些必要的準備工作,最後切換到服務使用者身份啟動服務。或者除了服務外,其它指令依舊可以使用 <code>root</code> 身份執行,友善調試等。

這些準備工作是和容器 <code>CMD</code> 無關的,無論 <code>CMD</code> 為什麼,都需要事先進行一個預處理的工作。這種情況下,可以寫一個腳本,然後放入 <code>ENTRYPOINT</code> 中去執行,而這個腳本會将接到的參數(也就是 <code>&lt;CMD&gt;</code>)作為指令,在腳本最後執行。比如官方鏡像 <code>redis</code> 中就是這麼做的:

可以看到其中為了 redis 服務建立了 redis 使用者,并在最後指定了 <code>ENTRYPOINT</code> 為 <code>docker-entrypoint.sh</code> 腳本。

該腳本的内容就是根據 <code>CMD</code> 的内容來判斷,如果是 <code>redis-server</code> 的話,則切換到 <code>redis</code> 使用者身份啟動伺服器,否則依舊使用 <code>root</code> 身份執行。比如:

繼續閱讀