iT邦幫忙

2025 iThome 鐵人賽

DAY 13
0
佛心分享-我的私藏工具箱

開發環境設定指南及工具分享系列 第 13

Day13-淺談Dockerfile-ENTRYPOINT與CMD:字串 vs 陣列格式深度解析

  • 分享至 

  • xImage
  •  

前言

在 Dockerfile 中,ENTRYPOINT 和 CMD 是兩個核心指令,用於定義容器啟動時執行的命令。然而,它們都有兩種不同的格式:Shell 格式 (字串) 和 Exec 格式 (陣列)。這兩種格式在底層的運作方式有著天壤之別,直接影響到容器的行為、信號處理 (Signal Handling) 和整體效能。

理解這兩者之間的差異,對於撰寫出健壯、可預測且易於維護的 Dockerfile 至關重要。本文將深入探討這兩種格式的區別,並提供最佳實踐建議。

兩種格式:Shell vs. Exec

1. Shell 格式 (Shell Form) - "字串"

Shell 格式是將整個命令寫成一個簡單的字串。

  • 語法INSTRUCTION "command param1 param2"

  • 範例CMD echo "Hello, Docker"

運作方式:

當你使用 Shell 格式時,Docker 會在內部將你的命令包裹在 /bin/sh -c 中執行。也就是說,CMD echo "Hello, Docker" 實際上等同於執行 /bin/sh -c 'echo "Hello, Docker"'。

優點:

  • 支援 Shell 特性:因為命令是在 Shell 環境中執行的,所以你可以直接使用環境變數替換、命令串接 (&&)、管道 (|) 等 Shell 功能。

缺點:

  • PID 1 問題:你的應用程式不會是容器中的主程序 (PID 1)。/bin/sh 會成為 PID 1,而你的應用程式則是它的一個子程序。這會導致一個嚴重問題:當你執行 docker stop 時,Docker 會向 PID 1 (也就是 /bin/sh) 發送 SIGTERM 信號。這個 Shell 程序可能不會正確地將信號轉發給你的應用程式,導致應用程式無法優雅地關閉 (graceful shutdown),最終被 SIGKILL 強制終止。

  • 解析問題:命令的解析完全交給 Shell,有時可能會產生非預期的行為。

2. Exec 格式 (Exec Form) - 

"陣列"

Exec 格式是將命令和參數寫成一個 JSON 陣列。

  • 語法INSTRUCTION ["executable", "param1", "param2"]

  • 範例CMD ["echo", "Hello, Docker"]

運作方式:

當你使用 Exec 格式時,Docker 會直接執行你指定的 executable,並傳入後續的參數。這個過程完全沒有 Shell 的參與。

優點:

  • 應用程式即為 PID 1:你指定的 executable 會直接成為容器的主程序 (PID 1)。這意味著 docker stop 發送的 SIGTERM 信號會直接被你的應用程式接收,讓你有機會進行清理工作,實現優雅關閉。

  • 行為明確:命令和參數的解析非常清晰,不會受到 Shell 的影響,行為更可預測。

缺點:

  • 不支援 Shell 特性:無法直接使用環境變數替換等 Shell 功能。如果需要,你必須明確地啟動一個 Shell,例如:CMD ["/bin/sh", "-c", "echo $HOME"]

ENTRYPOINT 與 CMD 的協同工作

ENTRYPOINT 和 CMD 可以獨立使用,也可以組合使用。當它們組合時,CMD 的值會成為 ENTRYPOINT 的預設參數。

ENTRYPOINT CMD 最終執行的命令 說明
(無) ["/bin/ls", "-l"] /bin/ls -l CMD 提供完整的執行命令。
["/usr/bin/wc"] ["-l"] /usr/bin/wc -l 最佳實踐ENTRYPOINT 定義主命令,CMD 提供預設參數。
"/bin/echo" "World" /bin/sh -c '/bin/echo' World CMD 的值會被附加到 ENTRYPOINT 命令的末尾。
["/bin/echo"] "Hello World" /bin/echo /bin/sh -c "Hello World" 這種組合方式通常不是我們想要的結果,應避免使用。

核心規則:

  • 當 ENTRYPOINT 使用 Exec 格式時,CMD 的值(無論是 Shell 還是 Exec 格式)都會被解析為 ENTRYPOINT 的參數。

  • 當 ENTRYPOINT 使用 Shell 格式時,CMD 的值會被完全忽略。

進階情境:docker exec 與 Dockerfile 繼承

ENTRYPOINT 和 CMD 的行為不僅僅影響 docker run,在容器的生命週期和映像檔的繼承中,它們的表現也值得我們深入探討。

1. docker exec 的行為

docker exec 指令的用途是在一個已經在運行的容器中執行一個新的命令。

一個常見的誤解是 docker exec 會以某種方式與 ENTRYPOINT 互動。事實上,docker exec 完全繞過了 ENTRYPOINT 和 CMD。這兩個指令只在容器啟動時docker run 或 docker create + docker start)被評估一次,用於設定容器的主程序(PID 1)。

docker exec 啟動的是一個全新的、獨立的程序,它與容器的主程序是兄弟關係。

簡單來說:

  • docker run [image] -> 觸發 ENTRYPOINT / CMD

  • docker exec [container] command -> 不觸發 ENTRYPOINT / CMD

範例:

假設你的 Dockerfile 如下:

FROM ubuntu
ENTRYPOINT ["top", "-b"]

你啟動容器 docker run -d --name my-top-container my-image,這個容器的主程序是 top -b

現在,如果你執行 docker exec my-top-container ls -l,你並不是在 ENTRYPOINT 的 top 程序中執行 ls -l。Docker 只是在容器的命名空間內直接啟動了一個新的 ls -l 程序。

2. Dockerfile 繼承 (FROM)

當你的 Dockerfile 從一個基礎映像檔(Base Image)繼承時,ENTRYPOINT 和 CMD 的指令會被繼承下來,但可以被覆蓋。規則如下:

  • CMD 的覆蓋:如果在新的 Dockerfile 中定義了 CMD,它將永遠覆蓋掉基礎映像檔中的 CMD

  • ENTRYPOINT 的覆蓋:如果在新的 Dockerfile 中定義了 ENTRYPOINT,它將永遠覆蓋掉基礎映像檔中的 ENTRYPOINT

這意味著,如果你只想修改基礎映像檔的預設參數,你只需要覆蓋 CMD 即可。

範例:

假設我們有一個基礎映像檔 base-image:

# Dockerfile for base-image
FROM ubuntu
ENTRYPOINT ["ping", "-c", "3"]
CMD ["localhost"]

這個映像檔預設會執行 ping -c 3 localhost

情境一:只覆蓋 CMD

# Dockerfile for new-image-1
FROM base-image
CMD ["google.com"]

new-image-1 將會繼承 ENTRYPOINT ["ping", "-c", "3"],但會用新的 CMD 作為參數。最終執行命令為:ping -c 3 google.com

情境二:覆蓋 ENTRYPOINT

# Dockerfile for new-image-2
FROM base-image
ENTRYPOINT ["curl"]

new-image-2 覆蓋了 ENTRYPOINT,但繼承了基礎映像檔的 CMD ["localhost"]。最終執行命令為:curl localhost。這是一個容易出錯的地方,因為繼承來的 CMD 可能不適用於新的 ENTRYPOINT

情境三:重置 ENTRYPOINT

如果你想完全擺脫基礎映像檔的 ENTRYPOINT,只使用 CMD,你可以將 ENTRYPOINT 設為一個空陣列。

# Dockerfile for new-image-3
FROM base-image
ENTRYPOINT []
CMD ["echo", "Hello from new image"]

new-image-3 將會執行 echo "Hello from new image",完全忽略了基礎映像檔的 ENTRYPOINT

實用範例:撰寫可擴充的 entrypoint.sh

在許多情境下,我們希望容器在啟動主應用程式之前,能先執行一些初始化腳本。同時,我們也希望能保有直接傳入參數,或是啟動一個 Shell 來進行偵錯的彈性。這可以透過撰寫一個 entrypoint.sh 腳本來完美實現。

1. entrypoint.sh 腳本範例

這個腳本的邏輯是檢查傳入的第一個參數 ($1)。如果這個參數看起來不像是要執行的命令(例如,它以 - 開頭,是一個選項),腳本就會在所有參數前加上預設的應用程式命令。否則,它就直接執行使用者傳入的命令。

#!/bin/sh
# 使用 `set -e` 確保腳本在任何指令失敗時立即退出
set -e

# 這裡可以放置你的初始化程式碼
echo "Running pre-start initializations..."
# 例如:等待資料庫連線、從 secret manager 取得機敏資料、產生設定檔等
# ...

# 判斷傳入的參數
# 邏輯:如果第一個參數以 `-` 開頭,或者第一個參數不是一個系統中存在的命令
# 我們就假設使用者想要執行預設的 CMD,並將傳入的參數作為 CMD 的參數。
# `command -v` 用於檢查命令是否存在
if [ "${1#-}" != "$1" ] || ! command -v "$1" > /dev/null; then
  # 在所有參數前加上預設的應用程式命令
  # 這裡的 `my-app` 是你容器中主要執行的程式
  set -- my-app "$@"
fi

# 使用 `exec` 來執行最終的命令
# `exec` 會用新的程序取代目前的 shell 程序,這確保了主應用程式
# 成為容器的 PID 1,從而能正確地接收來自 `docker stop` 的信號。
exec "$@"

2. Dockerfile 整合

接著,在 Dockerfile 中複製這個腳本並設定為 ENTRYPOINT

FROM ubuntu
# 假設你的應用程式叫做 my-app
# COPY ./my-app /usr/local/bin/my-app

# 複製 entrypoint 腳本並給予執行權限
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh

# 設定 entrypoint
ENTRYPOINT ["entrypoint.sh"]

# 提供預設執行的命令及參數
CMD ["--help"]

3. 運作方式拆解

透過這樣的組合,你可以實現極大的彈性:

  • 執行預設命令docker run my-image

    • entrypoint.sh 接收到 CMD 的內容,也就是 "--help"

    • 因為 "--help" 以 - 開頭,腳本會執行 set -- my-app --help

    • 最終 exec my-app --help 被執行。

  • 傳遞參數給預設命令docker run my-image --version

    • entrypoint.sh 接收到 "--version"

    • 同樣地,因為 "--version" 以 - 開頭,腳本會執行 set -- my-app --version

    • 最終 exec my-app --version 被執行。

  • 執行完全不同的命令(例如,進入 Shell 偵錯)docker run -it my-image bash

    • entrypoint.sh 接收到 "bash"

    • 腳本檢查發現 bash 是一個存在的命令 (command -v bash 成功)。

    • if 判斷式不成立,腳本直接跳到最後。

    • 最終 exec bash 被執行,使用者獲得一個互動式的 Shell。

  • docker exec 排查docker exec -it <container-id> bash

    • 如前述,docker exec 完全繞過 ENTRYPOINT,直接在容器內啟動 bash 程序,因此這種偵錯方式依然有效。

這個模式兼顧了標準化(初始化流程)與靈活性(命令覆蓋),是建構高品質 Docker 映像檔時非常推薦的作法。

最佳實踐與結論

根據以上的分析,我們可以得出以下結論和最佳實踐:

  1. 優先使用 Exec 格式:為了讓你的應用程式能正確接收系統信號並成為 PID 1,始終優先為 ENTRYPOINT 和 CMD 選擇 Exec 格式 (陣列)。

  2. 組合使用 ENTRYPOINT 和 CMD:將你的容器想像成一個可執行的程式。

    • 使用 ENTRYPOINT (Exec 格式) 來設定固定的、不會改變的主命令。

    • 使用 CMD (Exec 格式) 來為 ENTRYPOINT 提供一組預設的、可被覆蓋的參數。

範例 (推薦):

FROM ubuntu
ENTRYPOINT ["ping"]
CMD ["localhost"]
  • 預設執行 ping localhost

  • 使用者可以輕鬆覆蓋參數:docker run my-image google.com,這將會執行 ping google.com

透過遵循這些原則,你可以建立出行為一致、穩定且易於使用的 Docker 映像檔。


上一篇
12-淺談Dockerfile-3-multistage-build
系列文
開發環境設定指南及工具分享13
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言