為什麼 ENTRYPOINT 沒有被執行到?

今天朋友問了一個 Docker 問題很神奇,他說:為何 ENTRYPOINT 設定好了,但它沒有被執行?

苦主的狀況如下:

Dockerfile 基於 node:14.5.0-alpine,原始碼片段如下:

ENTRYPOINT [ "docker-entrypoint.sh" ]
CMD [ "yarn", "start" ]

docker-entrypoint.sh 內容如下:

#!/bin/sh
set -e
echo "DAMS Gateway starting..."
exec yarn --prod
exec "[email protected]"

苦主的問題是:他希望 docker run 的時候,一定要執行 yarn --prod,但不知道為何,怎麼試都無法成功。

追問題過程

首先當然是要試著重現看看。上面的說明已經有 shell script 完整檔案了,Dockerfile 就得模擬一下:

FROM node:14.5.0-alpine

# yarn 至少需要一個空的 package.json 檔
COPY package.json ./

COPY docker-entrypoint.sh /

ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["yarn", "start"]

然後 build and run:

docker build -t=test .
docker run --rm -it test whoami

確實 yarn --prod 沒有被執行,非常怪。

確認腳本運作正常

接著我第一步是確認 docker-entrypoint.sh 權限正常,以及進 container 可以正常使用 shell:

docker run --rm -it test bash

# Container 裡,先確認權限
ls -l /

# 執行 shell,確認腳本會執行 yarn --prod
sh -x /docker-entrypoint.sh whoami

腳本一切正常,但 docker run 就是不會出現 yarn --prod

改用 shell mode

想說會不會是環境變數的問題,於是就把 Dockerfile 改寫如下:

ENTRYPOINT docker-entrypoint.sh
CMD yarn start

這時突然有不一樣的結果:

$ docker run --rm -it test whoami
Welcome to Node.js v14.5.0.
Type ".help" for more information.
>

突然跑出 Node.js 的 REPL,咦?怎麼會是你?

這一瞬間想通了某件事,所以我把 Dockerfile 改回去,再執行一次指令試驗,終於知道為什麼了:

# 確認 docker-entrypoint.sh 檔案存在於根目錄
$ docker run --rm -it test ls /docker-entrypoint.sh
/docker-entrypoint.sh

# 確認 docker-entrypoint.sh 執行檔實際的位置
$ docker run --rm -it test which docker-entrypoint.sh
/usr/local/bin/docker-entrypoint.sh

原來我完全忘了 ENTRYPOINT ["docker-entrypoint.sh"] 這樣的寫法,會依照 PATH 環境變數找執行檔的位置並執行,所以這樣的寫法會執行 Node image 的 /usr/local/bin/docker-entrypoint.sh,而不是我的 Dockerfile 裡的 /docker-entrypoint.sh

會發生這個誤會的關鍵有兩個:

  1. 自定義的 entrypoint 剛好跟 image 的 entrypoint 撞名
  2. image 的 entrypoint 剛好放在 PATH 環境變數的路徑下

解法超級簡單,Dockerfile 加個斜線就好了:

ENTRYPOINT ["/docker-entrypoint.sh"]

結論

如果了解了上面的情境,接著想像一下,如果 base image CMD 設定如下:

CMD ["mycmd", "args"]

這時有人 FROM image 寫了下面這個 Dockerfile,而沒有覆寫 CMD 或 ENTRYPOINT:

# 另一個同名的 mycmd
COPY mycmd /usr/local/bin/mycmd

這個 Dockerfile build 出來的 image,執行就會出問題了。

然後才發現,官方 Dockerfile 不管是指令還是腳本,都會寫絕對路徑,原來就是為了避免今天遇到的這個問題。

參考資料