Docker Compose command 與 entrypoint 差異與覆寫規則
ENTRYPOINT 決定執行的程式,command 提供引數。本文整理 Compose 4 種覆寫組合,並說明設定 entrypoint 為何會清除映像的 CMD。
Docker Compose command 與 entrypoint:一項規則
在 Docker Compose 中,entrypoint:設定要執行的程式,command:設定要傳遞給該程式的引數。容器的程序由 entrypoint 清單組成,並將 command 清單附加在其末端。本頁的其他行為都源自這項規則。
這兩個鍵分別對應 Dockerfile 指令。entrypoint:會取代映像的 ENTRYPOINT。command:會取代映像的 CMD。兩者並非獨立運作,這正是容易造成混淆之處:設定 entrypoint:也會捨棄映像的 CMD。Compose 規格已直接說明這點。如果 entrypoint不是 null,Compose 會忽略映像的預設 command。
查看映像已宣告的內容
在覆寫任何設定前,先查看映像發布的內容。
docker image inspect --format '{{json .Config.Entrypoint}}' postgres:16
docker image inspect --format '{{json .Config.Cmd}}' postgres:16你會看到 ["docker-entrypoint.sh"] 和 ["postgres"],因此容器會執行 docker-entrypoint.sh postgres。該指令碼會在首次啟動時建立資料目錄、讀取 POSTGRES_* 變數、將權限降至 postgres 使用者,最後以 exec 執行傳入的引數。你想修改哪一部分,是整個決策的關鍵。若要將旗標傳給資料庫,請替換 command:。若替換 entrypoint:,上述設定流程將完全不會執行。
顯示於小型映像中的 4 種組合
建立一個唯一用途是列印啟動時所接收引數清單的映像。
FROM alpine:3.20
ENTRYPOINT ["/bin/echo", "ep"]
CMD ["cmd"]docker build -t argdemo .services:
demo:
image: argdemo每次修改後執行 docker compose up,並查看其記錄的單行內容。
- 兩個金鑰皆未設定。 程序為
/bin/echo ep cmd,記錄顯示ep cmd。 - 僅設定
command: ["cmd2"]。 程序為/bin/echo ep cmd2。entrypoint 未變更,只有引數變更。 - 僅設定
entrypoint: ["/bin/echo", "ep2"]。 程序為/bin/echo ep2,記錄顯示ep2。映像中的cmd已消失,且不會顯示任何警告。 - 兩個金鑰皆已設定。 程序為
/bin/echo ep2 cmd2。只有在此情況下,您才能完整控制引數清單。
設定 entrypoint 為何會清除映像的 CMD
映像的 CMD 會作為該映像 ENTRYPOINT 的預設引數清單。替換 entrypoint 後,這些引數會改為屬於已不再執行的程式,因此 Compose 會捨棄它們,而不是建立映像作者原本未預期的命令列。docker run --entrypoint 的行為也相同,因此這是 Docker 的行為,不是 Compose 的特殊問題。
其結果很明確。nginx:1.27 宣告了 ENTRYPOINT ["/docker-entrypoint.sh"] 和 CMD ["nginx", "-g", "daemon off;"]。設定 entrypoint: /custom-init.sh 後,您的指令碼會以空的引數清單啟動。以一般的 exec "$@" 結尾的指令碼此時沒有任何內容可供 exec,因此 exec 不會執行任何動作。指令碼會執行到最後一行,容器隨即以代碼 0 結束,且不會在任何地方顯示錯誤訊息。請自行補回引數:
services:
web:
image: nginx:1.27
entrypoint: /custom-init.sh
command: ["nginx", "-g", "daemon off;"]請記住這項規則:每當設定 entrypoint: 時,都要在同一次修改中決定 command: 應該設定為何。
exec form 與 shell form,以及 Compose 的差異
Dockerfile 接受兩種語法。CMD ["nginx", "-g", "daemon off;"] 是 exec form:直接執行二進位檔,不涉及 shell。CMD nginx -g "daemon off;" 是 shell form:Docker 會將其改寫為 /bin/sh -c 'nginx -g "daemon off;"',因此會先啟動 shell,而您的程式會成為它的子程序。
Compose 不會沿用這項規則,這點常讓人感到意外。command: 中的字串會拆分為引數並直接執行,不會加上 /bin/sh -c 包裝。Compose 參考文件明確指出,command 欄位不會在映像中定義的 SHELL context 內執行;如果需要 shell 功能,必須自行呼叫 shell。
因此,command: echo "hello $$HOSTNAME" 會輸出字面文字 hello $HOSTNAME。這個字串從未經過 shell,因此沒有任何內容會被展開。需要 shell 時,請明確指定:
services:
demo:
image: alpine:3.20
command: /bin/sh -c 'echo "hello $$HOSTNAME"'Signals、PID 1 與乾淨執行 docker compose down
docker compose stop 和 docker compose down 會在每個容器內向 PID 1 傳送 SIGTERM,等待 stop_grace_period,然後傳送 SIGKILL。預設寬限時間為 10 秒。
PID 1 在 Linux 中具有特殊性。核心不會將訊號的預設動作套用至 PID 1,因此未安裝 SIGTERM 處理常式的程序在以 PID 1 執行時,會直接忽略 SIGTERM。它會在完整寬限時間內持續執行,之後遭到強制終止,導致任何開啟中的連線或尚未提交的交易中斷。
在程式前方加入 shell 會提高這種情況發生的機率,因為 shell 是 PID 1,而大多數 shell 不會將訊號轉送給子程序。有些 shell 會在 -c 字串中以最後一個命令取代自身,因此程式有時仍會取得 PID 1。這取決於 shell 和確切字串,因此不要猜測。請直接查看:
docker compose exec -T web cat /proc/1/cmdline | tr '\0' ' '; echo如果 PID 1 顯示為 /bin/sh -c ...,而不是你的程式,則有兩種修正方式。在 image 中使用 exec form,或保留 shell,並使用 exec 將程序交給程式:
services:
web:
image: myapp:1.4
command: /bin/sh -c 'exec myapp --config /etc/myapp.toml'exec 會以你的程式取代 shell 程序,而不是 fork 子程序,因此你的程式會繼承 PID 1 並接收訊號。
有些程式會建立子程序卻從不回收,因為 PID 1 同時也是 reaper,這會留下 zombie process。Compose 提供了對應的開關:
services:
web:
image: myapp:1.4
init: true
stop_grace_period: 30sinit: true 會以 PID 1 執行小型 init process,將訊號轉送給你的程序並回收子程序。stop_grace_period 可讓真正緩慢的關機程序獲得更多時間。如果你的程式預期接收不同訊號,stop_signal: SIGQUIT 可變更 Compose 傳送的訊號。使用 docker image inspect --format '{{.Config.StopSignal}}' nginx:1.27 查看 image 目前要求的設定。
如果某個 stack 中的 docker compose down 每個服務都固定耗時 10 秒,表示沒有任何程序處理 SIGTERM。請先修正這個問題,再歸咎於工具;另請參閱docker compose down 與 stop 的差異,了解各子命令會移除哪些內容。
相同的 exec 與 shell 差異還會出現在另一個位置。以 test: ["CMD", "curl", "-f", "http://localhost/"] 撰寫的 healthcheck 會直接執行 binary,而 test: ["CMD-SHELL", "curl -f http://localhost/ || exit 1"] 會透過 shell 執行,因此 || 才具有作用。撰寫能誠實反映失敗的 Compose healthcheck會說明該欄位的其餘內容。
在官方映像上附加旗標
這正是多數讀者想了解的內容。你想在 postgres 上多加一個旗標,但不能影響初始化指令碼。
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
command: postgres -c max_connections=200 -c shared_buffers=256MB
volumes:
pgdata:只有 command: 有變更,因此 docker-entrypoint.sh 仍會執行,也仍會以 exec 方式執行你提供的內容。請檢查結果,不要直接假設設定已生效:
docker compose up -d db
docker compose exec -T db psql -U postgres -c 'show max_connections;'輸出應顯示 200。如果仍顯示 100,請執行 docker compose config,並確認你預期的 command 已包含在合併後的輸出中。Compose 合併 override 檔案時,會直接取代 command,而不是將內容附加到其中,因此另一個同樣設定 command: 的檔案會無聲地覆寫原有設定。
上述的 ${POSTGRES_PASSWORD} 由 Compose 在主機上,根據你的 .env 檔案展開,且發生在容器建立之前。Compose 中的環境檔案與 secrets 說明該值可以安全地存放在哪裡。
使用 docker compose run 執行一次性遷移
docker compose run會根據相同的服務定義建立新容器,並將命令替換為服務名稱後方的輸入內容。映像的 entrypoint 仍會執行,因此容器的準備方式與長時間執行的服務完全相同。
docker compose run --rm app python manage.py migrate--rm會在命令結束時刪除容器。若未使用此選項,每次執行都會留下已停止的容器,並可在docker compose ps -a中查看。- 不會發布連接埠。
run容器會忽略服務的ports:,除非加入--service-ports,因此不會與已在執行的服務發生連接埠衝突。 - 會先啟動相依服務。
depends_on中的所有項目都會在命令執行前啟動,而--no-deps會略過這項行為。 - 容器會取得產生的名稱,例如
myproject-app-run-9f2c1a,因此不會與服務容器名稱衝突。
若也要替換 entrypoint,可使用下列旗標:
docker compose run --rm --entrypoint /bin/sh app -c 'python manage.py migrate'產生的引數清單是 /bin/sh -c 'python manage.py migrate',因為服務名稱後方的文字仍會被視為命令。docker compose exec是另一個工具,運作方式不同:它會在已經執行中的容器內執行程序,並完全忽略 entrypoint:與 command:。需要新容器來執行工作時,使用 run;需要查看執行中容器的內容時,使用 exec。Compose 命令速查表會並列其餘子命令。
為什麼我的容器會立即結束?
先查看結束碼,因為這能快速縮小原因範圍。
docker compose ps -a
docker compose logs app結束碼為 0 且沒有輸出。 命令已執行完畢。最常見的原因是 entrypoint: override 同時覆寫了映像的 CMD,導致 entrypoint 以空的引數清單執行,沒有任何內容可交接。
錯誤訊息以 permission denied 結尾。 映像中的指令檔沒有可執行位元,通常是因為儲存庫中的檔案從未設定該位元。請在建置時使用 COPY --chmod=0755 entrypoint.sh /entrypoint.sh 設定。
對於映像中明明看得到的檔案,錯誤訊息卻以 no such file or directory 結尾。 該指令檔使用了 Windows 換行字元。此時第一行會讀成 #!/bin/sh 加上 carriage return 位元組,因此 kernel 會尋找名稱中包含該位元組的直譯器,結果找不到。請執行 dos2unix entrypoint.sh,然後將 * text eol=lf 加入 .gitattributes,避免問題再次發生。
executable file not found in $PATH。 command: 中指定的二進位檔不在映像中,或是你寫入了 cd 這類 shell 內建命令,但此處只能使用真正的程式。
取得 entrypoint 失敗之映像中的 shell
如果 entrypoint 在你檢查任何內容前就結束,請替換它:
docker compose run --rm --entrypoint /bin/sh app如果這會傳回 executable file not found in $PATH,表示映像完全沒有 shell。Distroless 與以 scratch 為基礎的映像通常不會隨附 shell。即使不啟動 entrypoint,你仍可從外部讀取檔案系統:
docker create --name probe myapp:1.4
docker export probe | tar -tv | head -40
docker rm probe如果需要讓容器持續執行,以便反覆附加至容器,請讓它執行一個永不結束的程序。將以下內容放在不提交至版本控制的 override 檔案中:
services:
app:
entrypoint: ["tail", "-f", "/dev/null"]
command: []嚴格來說不需要 command: [],因為設定 entrypoint: 已清除映像的 CMD;但明確寫出來,可以記錄此意圖,方便下一位閱讀該檔案的人理解。啟動容器後進入其中:
docker compose -f compose.yaml -f compose.debug.yaml up -d app
docker compose exec app /bin/sh現在手動執行真正的 entrypoint,並監看它在哪裡停止。如此一來,錯誤訊息會顯示在你的終端機中,而不是出現在半秒前就已結束的容器裡。如果你仍在組建第一個 stack,在 VPS 上建立第一個 Compose stack 涵蓋了上述內容所假設的檔案配置。
FAQ
為什麼執行 docker compose up 後,容器會立即結束?
檢查 docker compose ps -a 取得結束代碼。若結束代碼為 0 且沒有輸出,通常表示您在服務上設定了 entrypoint:,這也會清除映像的 CMD,因此 entrypoint 會以空的引數列表執行並結束。使用 command: 加回引數。若錯誤訊息以 permission denied 結尾,表示 entrypoint script 沒有 executable bit。若檔案確實存在但錯誤訊息以 no such file or directory 結尾,表示 script 使用 Windows 換行字元,因此其 shebang 行指定的 interpreter 不存在。
在 Compose 中設定 entrypoint 會移除映像的 CMD 嗎?
會。若 entrypoint 不是 null,Compose 會忽略映像宣告的預設 command。這是有文件記載的行為,也符合 docker run --entrypoint。原因是映像的 CMD 是以該映像 ENTRYPOINT 的引數形式撰寫,因此替換 entrypoint 後,原有引數便不再對應任何項目。若新的 entrypoint 仍需要引數,請在同一個服務中設定 command:。
Compose 中的字串 command 會透過 shell 執行嗎?
不會。不同於 Dockerfile 的 CMD,Compose 中的字串 command: 會拆分為引數並直接執行,不會使用 /bin/sh -c wrapper。因此 $VARIABLE 不會由容器內的 shell 展開。需要使用 shell 時,請自行呼叫 shell,例如 command: /bin/sh -c 'echo "hello $$HOSTNAME"'。成對的 $$ 會跳脫 dollar sign,讓 Compose 將其傳遞至容器,而不是在 host 上展開。
為什麼 docker compose down 讓單一容器花費 10 秒才結束?
Compose 會將 SIGTERM 傳送至 PID 1,等待 stop_grace_period(預設為 10 秒),然後再傳送 SIGKILL。kernel 不會對 PID 1 套用預設的 signal 行為,因此沒有 SIGTERM handler 的程式會忽略該 signal,並一律等待完整的期間。使用 docker compose exec -T app cat /proc/1/cmdline | tr '\0' ' ' 查明 PID 1 實際執行的程式。若它是 shell,請將映像切換為 exec form,或在 shell 字串中寫入 exec。若該 process 會產生卻從不 reaping child processes,請在服務上設定 init: true。