寫 Dockerfile 時,經常搞不清楚 CMD
與 ENTRYPOINT
的差異,雖然兩者用途相當雷同,但還是特別查閱了一下 Docker 的官方文件,了解兩者的用途以及差別,並且透過實際的範例驗證後,記錄成本文。
本文環境
- macOS 11.6
- Docker Desktop 4.3.1
Exec form vs. Shell form
在理解 CMD
與 ENTRYPOINT
的差異之前,必須先了解 2 個重要的格式:
- Shell form
command param1 param2
- Exec form
["executable","param1","param2"]
上述 2 種都是 CMD
與 ENTRYPOINT
可支援的格式。
以執行 ls -alh
指令為例, shell form 可以直接將 ls -alh
接在 CMD
以及 ENTRYPOINT
後方即可,就像執行 shell 一樣,例如:
CMD ls -alh
vs.
ENTRYPOINT ls -alh
而 exec form 則需要將指令以 JSON 陣列(array)方式進行表達,例如 ls -alh
指令就必須轉換為 ["ls", "-alh"]
, 例如:
CMD ["ls", "-alh"]
vs.
ENTRYPOINT ["ls", "-alh"]
此外, exec form 也是 Docker 較推薦使用的方式,所以一般會較少看到開發者使用 shell form 。
CMD
以下引用自 Docker 官方文件,說明 CMD
的用途:
The main purpose of a
CMD
is to provide defaults for an executing container. These defaults can include an executable, or they can omit the executable, in which case you must specify anENTRYPOINT
instruction as well.
簡而言之,CMD
主要用途是為執行容器(container)時提供預設值,預設值可以是一個可執行檔(executable)以及其需要的參數;也可以只是單純提供參數的部分,不過這種用法一定要搭配 ENTRYPOINT
使用,也就是 CMD
只是作為 ENTRYPOINT
所需的預設參數。
CMD
的範例可以參考 Python:3.10.1-alpine3.15 的 Dockerfile:
CMD ["python3"]
該 Dockerfile 只有用 CMD
提供 1 個 python3
作為可執行檔(executable) , 因此執行該 Python 的容器(container)時就會啟動 Python 直譯器(interpreter):
$ docker run -it python:3.10.1-alpine3.15
Python 3.10.1 (main, Dec 8 2021, 04:32:13) [GCC 10.3.1 20211027] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>
CMD 的相關規則
Dockerfile should specify at least one of
CMD
orENTRYPOINT
commands.
There can only be one
CMD
instruction in a Dockerfile. If you list more than oneCMD
then only the lastCMD
will take effect.
Dockerfile 撰寫規則中規定必須包含至少 1 個 CMD
或 ENTRYPOINT
在內。如果有多個 CMD
指令被寫在 Dockerfile 內,只有最後 1 個有效。
但以下的 Dockerfile 範例不包含任何 CMD
或 ENTRYPOINT
也能夠通過這個規則:
# Dockerfile
FROM python:3.10.1-alpine3.15
上述 Dockerfile 編譯指令:
$ docker build -t mypy:latest .
這是由於 python:3.10.1-alpine3.15
中已經包含 CMD
的原因,當我們以 python:3.10.1-alpine3.15
為基底(base)時,就會繼承其 CMD
或 ENTRYPOINT
, 如果想覆蓋原始的 CMD
或 ENTRYPOINT
設定,只需要明確指定 CMD
或 ENTRYPOINT
即可,例如:
# Dockerfile
FROM python:3.10.1-alpine3.15
CMD ["echo", "$HOME"]
上述 Dockerfile 編譯與執行結果如下:
$ docker build -t mypy:latest .
$ docker run mypy:latest
$HOME
從上述結果可以發現:環境變數 $HOME 並沒有成功被列印!
這是由於 exec form 內的指令並不是透過 shell 環境所執行的原因,因此若要讓環境變數也能夠生效,就必須使用 shell form 設定 CMD
, 或者明確以 ["/bin/sh", "-c", "<command> [param1] [param2]..."]
方式設定 CMD
, 例如以下 2 個範例,都能達到相同效果:
範例 1:
# Dockerfile
FROM python:3.10.1-alpine3.15
CMD echo $HOME
範例 2:
# Dockerfile
FROM python:3.10.1-alpine3.15
CMD ["/bin/sh", "-c", "echo $HOME"]
上述 2 個範例編譯與執行結果如下:
$ docker build -t mypy:latest .
$ docker run mypy:latest
/root
最後,當我們執行 docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
指令時,其實等同於設定 CMD
:
If the user specifies arguments to
docker run
then they will override the default specified inCMD
.
值得注意的是,以 docker run
指令執行 CMD
也同樣會遇到前述環境變數的問題,如下 2 種指令所示,可以發現 1 個無法正確列印環境變數,另 1 個則可以:
$ docker run -it python:3.10.1-alpine3.15 echo \$PYTHON_VERSION
$PYTHON_VERSION
$ docker run -it python:3.10.1-alpine3.15 /bin/sh -c "echo \$PYTHON_VERSION"
3.10.1
ENTRYPOINT
在沒有同時使用 CMD
以及 ENTRYPOINT
的情況下,基本上可以說 CMD
以及 ENTRYPOINT
擁有相同的功用,都能夠讓容器(container)啟動時執行特定程式(當然也同樣支援 shell form 以及 exec form)。
但兩者仍有區別,根據 Docker 官方文件所言,如果要將整個容器當作可執行檔(executable)時,就應該使用 ENTRYPOINT
。
ENTRYPOINT
should be defined when using the container as an executable.
單就敘述而言,其實不好理解 “using the container as an executable” 的意思,但透過範例將會很好理解。
以下 Dockerfile 範例透過 ENTRYPOINT
在容器啟動時,執行 Python 的 HTTP server:
# Dockerfile
FROM python:3.10.1-alpine3.15
ENTRYPOINT ["python", "-m", "http.server"]
以下是編譯以及執行的結果:
$ docker build -t mypy:latest .
$ docker run -it mypy:latest
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
接著,試著透過 docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
指令執行 mypy:latest
內的 ls
試試:
$ docker run -it mypy:latest ls
usage: server.py [-h] [--cgi] [--bind ADDRESS] [--directory DIRECTORY] [port]
server.py: error: argument port: invalid int value: 'ls'
從上述結果可以看到使用 ENTRYPOINT
後,就無法再像 CMD
那樣輕易執行容器內的相關指令, docker run
指令中的 [COMMAND] [ARG...]
部分被附加(append)到 ENTRYPOINT
後並執行,讓整個容器就像被包裝成可執行檔(executable)一樣, docker run
指令中的 [COMMAND] [ARG...]
對該容器而言就是參數,這也是為何上述結果出現 error: argument port: invalid int value: 'ls'
錯誤訊息,這是由於最後容器執行指令變成 python -m http.server ls
的緣故。
那如果使用了 ENTRYPOINT
, 同時也想要具備可以任意執行容器內的指令的能力時,該怎麼辦?
可以參考 NGINX 的做法,該 Dockerfile 使用 ENTRYPOINT ["entrypoint.sh"]
處理透過 docker run
指令傳進來的字串,在字串符合特定規則時,觸發相對應的行為,否則就統一交給 exec "$@"
執行:
#!/bin/sh
# vim:sw=4:ts=4:et
set -e
if [ -z "${NGINX_ENTRYPOINT_QUIET_LOGS:-}" ]; then
exec 3>&1
else
exec 3>/dev/null
fi
if [ "$1" = "nginx" -o "$1" = "nginx-debug" ]; then
if /usr/bin/find "/docker-entrypoint.d/" -mindepth 1 -maxdepth 1 -type f -print -quit 2>/dev/null | read v; then
(...略...)
fi
fi
exec "$@"
這就是為什麼 NGINX 雖然使用 ENTRYPOINT
, 但仍能任意執行容器內的指令的原因,例如以下執行 NGINX 容器內的 ls -alh
指令:
docker run -it --rm nginx ls -alh
total 80K
drwxr-xr-x 1 root root 4.0K Dec 27 16:04 .
drwxr-xr-x 1 root root 4.0K Dec 27 16:04 ..
-rwxr-xr-x 1 root root 0 Dec 27 16:04 .dockerenv
drwxr-xr-x 2 root root 4.0K Dec 20 00:00 bin
drwxr-xr-x 2 root root 4.0K Dec 11 17:25 boot
drwxr-xr-x 5 root root 360 Dec 27 16:04 dev
drwxr-xr-x 1 root root 4.0K Dec 21 03:00 docker-entrypoint.d
-rwxrwxr-x 1 root root 1.2K Dec 21 03:00 docker-entrypoint.sh
drwxr-xr-x 1 root root 4.0K Dec 27 16:04 etc
drwxr-xr-x 2 root root 4.0K Dec 11 17:25 home
drwxr-xr-x 1 root root 4.0K Dec 20 00:00 lib
drwxr-xr-x 2 root root 4.0K Dec 20 00:00 lib64
drwxr-xr-x 2 root root 4.0K Dec 20 00:00 media
drwxr-xr-x 2 root root 4.0K Dec 20 00:00 mnt
drwxr-xr-x 2 root root 4.0K Dec 20 00:00 opt
dr-xr-xr-x 186 root root 0 Dec 27 16:04 proc
drwx------ 2 root root 4.0K Dec 20 00:00 root
drwxr-xr-x 3 root root 4.0K Dec 20 00:00 run
drwxr-xr-x 2 root root 4.0K Dec 20 00:00 sbin
drwxr-xr-x 2 root root 4.0K Dec 20 00:00 srv
dr-xr-xr-x 13 root root 0 Dec 27 16:04 sys
drwxrwxrwt 1 root root 4.0K Dec 21 03:00 tmp
drwxr-xr-x 1 root root 4.0K Dec 20 00:00 usr
drwxr-xr-x 1 root root 4.0K Dec 20 00:00 var
如果好奇容器正在執行何種指令,可以透過 docker ps --no-trunc
指令顯示。
以下為執行 NGINX 後,並透過 docker ps --no-trunc
列印容器執行指令的範例:
$ docker run -it -d --rm -p 8888:80 nginx nginx-debug -g 'daemon off;'
$
$ docker ps --no-trunc --format "{{.Image}} {{.Command}}"
nginx "/docker-entrypoint.sh nginx-debug -g 'daemon off;'"
順帶一提,除了 NGINX 的做法之外,其實也可以用 docker run
指令中的 --entrypoint
參數覆蓋改寫 ENTRYPOINT
,如此就能夠任意執行容器內的指令:
$ docker run -it --rm --entrypoint /bin/sh nginx
CMD 與 ENTRYPOINT 的交互作用表
最後, Docker 官方文件其實有整理 CMD
與 ENTRYPOINT
的表格,讓大家了解 CMD
以及 ENTRYPOINT
如何交互作用:
以上!
Happy Coding!
References
https://docs.docker.com/engine/reference/builder/#cmd
https://docs.docker.com/engine/reference/builder/#entrypoint