Jamie Chien • 2024-01-07
Docker: CMD 和 ENTRYPOINT 的差異
前言
在學習 Docker 的時候,看到 CMD 和 ENTRYPOINT 這兩個指令,這兩個用途很雷同,但一直搞不懂之間的差異,所以稍微研究了一下,並在這邊將我的理解記錄下來,如有錯誤,還請大家可以在底下留言指教~
Shell vs. Exec Form
RUN, ENTRYPOINT 和 CMD 這三個指令都有兩個不同的形式,這些形式會影響每個指令的行為
Shell form
指令是不帶 [] 的,且由 container 的 shell (e.g. /bin/sh -c) 執行
FROM node:18-alpine
# /bin/sh -c 'echo $HOME'
RUN echo $HOME
# /bin/sh -c 'echo $PATH'
CMD echo $PATH
Exec form
指令是以 [] 包裹的形式撰寫,並直接執行,而非透過 shell
FROM node:18-alpine
RUN ["npm", "install"]
CMD ["npm", "start"]
建議
以下為推薦使用在各個指令的形式
RUN: Shell Form (因為 shell feature)ENTRYPOINT: Exec Form (因為 signal trapping)CMD: Exec Form (因為 signal trapping)
建議使用 Exec form,因為他可以避免潛在的 Shell 注入問題,並更直接的命令和給參數,而無須透過 Shell,下面有詳細的建議說明
若命令需要使用 Shell 的功能,建議寫成 shell script,並透過 Exec form 的形式來執行
CMD
CMD 是用來設定一個命令 (command),並在啟動 container (from Image) 的時候執行此命令
CMD存在的目的是為了提供執行中的 container 一個預設值,此預設值可以是一個可執行檔(executable),或者也可以省略可執行檔,只留下參數,但在後者的情況下,必須同時指定ENTRYPOINT指令
Example
# Exec form
CMD ["executable", "param1", "param2"]
# Exec form (作為 ENTRYPOINT 的預設參數)
CMD ["param1", "param2"]
# Shell form
CMD command param1 param2
特性
- 必須包含至少一個
CMD或ENTRYPOINT - 若 Dockerfile 中包含多於一個
CMD時,只有最後一個CMD會有作用 - 若在
docker run的指令後面定義參數,那將會覆蓋掉 Dockerfile 中定義的CMD(但依舊會使用在 Dockerfile 中定義的預設ENTRYPOINT)
請不要搞混 RUN 和 CMD, RUN 實際上會執行一個 command 並且對結果做 commit; 但 CMD 在 build
階段不會執行任何指令,但會定義預期的 command 給 Image
ENTRYPOINT
ENTRYPOINT 允許你設置一個 container,並將此 container 作為一個可執行檔(executable)來運行
Example
# Exec form (preferred)
ENTRYPOINT ["executable", "param1", "param2"]
# Shell form
ENTRYPOINT command param1 param2
特性
- 必須包含至少一個
CMD或ENTRYPOINT - 若 Dockerfile 中包含多於一個
ENTRYPOINT時,只有最後一個ENTRYPOINT會有作用 - 若在
docker run的指令後面透過--entrypoint定義參數,將覆蓋掉 Dockerfile 中定義的ENTRYPOINT - 使用 Shell form 形式的
ENTRYPOINT指令,會忽略任何CMD以及 command line 的參數設定,並且會使用/bin/sh -c作為 subcommand 來運行ENTRYPOINT
CMD 與 ENTRYPOINT 的交互作用
當 CMD 和 ENTRYPOINT 同時被定義的時候,CMD 的含意就改變了,不再是直接執行其指令,而是做為參數傳遞給 ENTRYPOINT,概念上就變成:
<ENTRYPOINT> "<CMD>"
至於在運行 container 時,哪個指令會被執行呢? 以下會描述一些他們合作的規則:
- Dockerfile 中至少需要包含一個
CMD或ENTRYPOINT - 若要將 container 作為一個可執行檔(executable)時,就必須要定義
ENTRYPOINT CMD應該被用來定義ENTRYPOINT的默認參數,或者用於 container 中執行 ad-hoc command- 當在
docker run後面加上參數時,CMD將被覆蓋
以下的表格展示不同的 CMD/ENTRYPOINT 組合,實際上會執行什麼樣的 command:
No ENTRYPOINT | ENTRYPOINT exec_entry p1_entry | ENTRYPOINT ["exec_entry", "p1_entry"] | |
|---|---|---|---|
No CMD | error, not allowed | /bin/sh -c exec_entry p1_entry | exec_entry p1_entry |
CMD ["exec_cmd", "p1_cmd"] | exec_cmd p1_cmd | /bin/sh -c exec_entry p1_entry | exec_entry p1_entry exec_cmd p1_cmd |
CMD exec_cmd p1_cmd | /bin/sh -c exec_cmd p1_cmd | /bin/sh -c exec_entry p1_entry | exec_entry p1_entry /bin/sh -c exec_cmd p1_cmd |
上面說的落落長,但為什麼有了 CMD 後還需要 ENTRYPOINT 呢?
參考這篇文章提到的情境後就會清楚許多~
該使用 CMD 還是 ENTRYPOINT
CMD 和 ENTRYPOINT 對於建構和執行 Dockerfile 來說都是不可或缺的,使用情境上完全取決於當下的情況,但一般來說:
- 使用
RUN來安裝依賴和模組,並建構 Image ENTRYPOINT適合使用在當 container 啟動時需要始終運行特定命令的情況CMD適合在沒有定義任何 arguments 給docker run時作為預設的執行指令CMD也適合作為預設參數來給ENTRYPOINT,同時也可透過在docker run後面加上參數來替換 Dockerfile 中的CMD指令
不論 CMD 或 ENTRYPOINT,只要使用 Shell form,Docker 預設會使用 /bin/sh -c 來執行指令
為什麼建議使用 Exec form (補充)
以下會用 Shell form 形式來舉一個反例:
# Shell form
ENTRYPOINT npm run dev
當我在 Dockerfile 使用以上指令時,事實上相當於在 container 中運行:
/bin/sh -c npm run dev
因此,在這個例子中 /bin/sh -c 會是 main process,而 npm run dev 會被作為參數傳遞進去。因為 main process 是 /bin/sh -c 的原因,所以會導致某些信號處理(Signal Handling)和終止(Termination)的問題出現
在 Docker 中,container 裡面的 main process 通常被稱為 “init” process,且它通常以 PID 1 運行
Signal Handling
在 container 中的 PID 1 要負責處理信號(Signals),例如: 在執行 docker stop 時會出現SIGTERM 這個終止信號。然而,大多數的 shells 不會發送 signals 給 child process,因此可能會導致出現非預期的行為。
結論
根據上面的例子,這時候 container 裡面的 PID 1 就會是 shell process (/bin/sh -c),而 npm run dev 這個指令的執行會是他的 child process,因此,在操作一些信號處理的指令時,可能會發現沒有作用,這時候就可以快速的猜出可能的問題了~