Dockerfile: ENTRYPOINT和CMD的區別
翻譯:Dockerfile: ENTRYPOINT vs CMD
在我們查閱Dockerfile的官方文檔時, 有可能發現一些命令的功能重複(至少看起來乾的事情差不多), 我已經在上文分析過ADD和COPY命令的區別(他們功能類似), 現在我們分析另外2個命令, 他們的功能也非常類似, 是CMD和ENTRYPOINT.
儘管ENTRYPOINT和CMD都是在docker image里執行一條命令, 但是他們有一些微妙的區別. 在絕大多數情況下, 你只要在這2者之間選擇一個調用就可以. 但他們有更高級的應用, CMD和ENTRYPOINT組合起來使用, 完成更加豐富的功能.
ENTRYPOINT還是CMD?
從根本上說, ENTRYPOINT和CMD都是讓用戶指定一個可執行程序, 這個可執行程序在container啟動後自動啟動. 實際上, 如果你想讓自己製作的鏡像自動運行程序(不需要在docker run後面添加命令行指定運行的命令), 你必須在Dockerfile裡面, 使用ENTRYPOINT或者CMD命令
比如執行運行一個沒有調用ENTRYPOINT或者CMD的docker鏡像, 一定返回錯誤
$ docker run alpineFATA[0000] Error response from daemon: No command specified
大部分Linu發行版的基礎鏡像裡面調用CMD命令, 指定容器啟動後執行/bin/sh或/bin/bash. 這樣鏡像啟動默認進入互動式的shell
譯註: 3個不同的Linux鏡像(ubuntu, busybox, debian)都在Dockerfile的最後調用 CMD /bin/bash
啟動Linux發行版的基礎container後, 默認調用shell程序, 符合大多數人的習慣.
但是, 作為開發者, 你希望在docker鏡像啟動後, 自動運行其他程序. 所以, 你需要用CMD或者ENTRYPOINT命令顯式地指定具體的命令.
覆蓋(Overrides)
在寫Dockerfile時, ENTRYPOINT或者CMD命令會自動覆蓋之前的ENTRYPOINT或者CMD命令.
在docker鏡像運行時, 用戶也可以在命令指定具體命令, 覆蓋在Dockerfile里的命令.
比如, 我們寫了一個這樣的Dockerfile:
FROM ubuntu:trustyCMD ping localhost
如果根據這個Dockerfile構建一個新image, 名字叫demo
$ docker run -t demoPING localhost (127.0.0.1) 56(84) bytes of data.64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.051 ms64 bytes from localhost (127.0.0.1): icmp_seq=2 ttl=64 time=0.038 ms^C--- localhost ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 999msrtt min/avg/max/mdev = 0.026/0.032/0.039/0.008 ms
可以看出ping命令在docker啟動後自己執行, 但是我們可以在命令行啟動docker鏡像時, 執行其他命令行參數, 覆蓋默認的CMD
$ docker run demo hostname6c1573c0d4c0
docker啟動後, 並沒有執行ping命令, 而是運行了hostname命令
和CMD類似, 默認的ENTRYPOINT也在docker run時, 也可以被覆蓋. 在運行時, 用--entrypoint覆蓋默認的ENTRYPOINT
$ docker run --entrypoint hostname demo075a2fa95ab7
因為CMD命令很容易被docker run命令的方式覆蓋, 所以, 如果你希望你的docker鏡像的功能足夠靈活, 建議在Dockerfile里調用CMD命令. 比如, 你可能有一個通用的Ruby鏡像, 這個鏡像啟動時默認執行irb (CMD irb).
如果你想利用這個Ruby鏡像執行任何Ruby腳本, 只需要執行這句話:
docker run ruby ruby -e puts "Hello"
譯註: ruby -e puts "Hello" 覆蓋了 irb 命令
相反, ENTRYPOINT的作用不同, 如果你希望你的docker鏡像只執行一個具體程序, 不希望用戶在執行docker run的時候隨意覆蓋默認程序. 建議用ENTRYPOINT.
Docker在很多情況下被用來打包一個程序. 想像你有一個用python腳本實現的程序, 你需要發布這個python程序. 如果用docker打包了這個python程序, 你的最終用戶就不需要安裝python解釋器和python的庫依賴. 你可以把所有依賴工具打包進docker鏡像里, 然後用
ENTRYPOINT指向你的Python腳本本身. 當然你也可以用CMD命令指向Python腳本. 但是通常用ENTRYPOINT可以表明你的docker鏡像只是用來執行這個python腳本,也不希望最終用戶用這個docker鏡像做其他操作.
在後文會介紹如何組合使用ENTRYPOINT和CMD. 他們各自獨特作用會表現得更加明顯.
Shell vs. Exec
ENTRYPOINT和CMD指令支持2種不同的寫法: shell表示法和exec表示法. 下面的例子使用了shell表示法:
CMD executable param1 param2
當使用shell表示法時, 命令行程序作為sh程序的子程序運行, docker用/bin/sh -c的語法調用. 如果我們用docker ps命令查看運行的docker, 就可以看出實際運行的是/bin/sh -c命令
$ docker run -d demo15bfcddb11b5cde0e230246f45ba6eeb1e6f56edb38a91626ab9c478408cb615$ docker ps -lCONTAINER ID IMAGE COMMAND CREATED15bfcddb4312 demo:latest "/bin/sh -c ping localhost" 2 seconds ago
我們再次運行demo鏡像, 可以看出來實際運行的命令是/bin/sh -c ping localhost.
雖然shell表示法看起來可以順利工作, 但是它其實上有一些小問題存在. 如果我們用docker ps命令查看正在運行的命令, 會有下面的輸出:
$ docker exec 15bfcddb ps -fUID PID PPID C STIME TTY TIME CMDroot 1 0 0 20:14 ? 00:00:00 /bin/sh -c ping localhostroot 9 1 0 20:14 ? 00:00:00 ping localhostroot 49 0 0 20:15 ? 00:00:00 ps -f
PID為1的進程並不是在Dockerfile裡面定義的ping命令, 而是/bin/sh命令. 如果從外部發送任何POSIX信號到docker容器, 由於/bin/sh命令不會轉發消息給實際運行的ping命令, 則不能安全得關閉docker容器(參考更詳細的文檔:Gracefully Stopping Docker Containers).
譯註: 在上面的ping的例子中, 如果用了shell形式的CMD, 用戶按ctrl-c也不能停止ping命令, 因為ctrl-c的信號沒有被轉發給ping命令
除了上面的問題, 如果你想build一個超級小的docker鏡像, 這個鏡像甚至連shell程序都可以沒有. shell的表示法沒辦法滿足這個要求. 如果你的鏡像裡面沒有/bin/sh, docker容器就不能運行.
A better option is to use the exec form of the ENTRYPOINT/CMD instructions which looks like this:
一個更好的選擇是用exec表示法:
CMD ["executable","param1","param2"]
Lets change our Dockerfile from the example above to see this in action:
CMD指令後面用了類似於JSON的語法表示要執行的命令. 這種用法告訴docker不需要調用/bin/sh執行命令.
我們修改一下Dockerfile, 改用exec表示法:
FROM ubuntu:trustyCMD ["/bin/ping","localhost"]
重新build鏡像, 用docker ps命令檢查效果:
$ docker build -t demo .[truncated]$ docker run -d demo90cd472887807467d699b55efaf2ee5c4c79eb74ed7849fc4d2dbfea31dce441$ docker ps -lCONTAINER ID IMAGE COMMAND CREATED90cd47288780 demo:latest "/bin/ping localhost" 4 seconds ago
現在沒有啟動/bin/sh命令, 而是直接運行/bin/ping命令, ping命令的PID是1. 無論你用的是ENTRYPOINT還是CMD命令, 都強烈建議採用exec表示法,
ENTRYPOINT 和 CMD組合使用
之前只討論了用ENTRYPOINT或者CMD之一指定image的默認運行程序, 但是在某種情況下, 組合ENTRYPOINT和CMD能發揮更大的作用.
組合使用ENTRYPOINT和CMD, ENTRYPOINT指定默認的運行命令, CMD指定默認的運行參數. 例子如下:
FROM ubuntu:trustyENTRYPOINT ["/bin/ping","-c","3"]CMD ["localhost"]
根據上面的Dockerfile構建鏡像, 不帶任何參數運行docker run命令
$ docker build -t ping .[truncated]$ docker run pingPING localhost (127.0.0.1) 56(84) bytes of data.64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.025 ms64 bytes from localhost (127.0.0.1): icmp_seq=2 ttl=64 time=0.038 ms64 bytes from localhost (127.0.0.1): icmp_seq=3 ttl=64 time=0.051 ms--- localhost ping statistics ---3 packets transmitted, 3 received, 0% packet loss, time 1999msrtt min/avg/max/mdev = 0.025/0.038/0.051/0.010 ms$ docker ps -lCONTAINER ID IMAGE COMMAND CREATED82df66a2a9f1 ping:latest "/bin/ping -c 3 localhost" 6 seconds ago
上面執行的命令是ENTRYPOINT和CMD指令拼接而成. ENTRYPOINT和CMD同時存在時, docker把CMD的命令拼接到ENTRYPOINT命令之後, 拼接後的命令才是最終執行的命令. 但是由於上文說docker run命令行執行時, 可以覆蓋CMD指令的值. 如果你希望這個docker鏡像啟動後不是ping localhost, 而是ping其他伺服器,, 可以這樣執行docker run:
$ docker run ping docker.ioPING docker.io (162.242.195.84) 56(84) bytes of data.64 bytes from 162.242.195.84: icmp_seq=1 ttl=61 time=76.7 ms64 bytes from 162.242.195.84: icmp_seq=2 ttl=61 time=81.5 ms64 bytes from 162.242.195.84: icmp_seq=3 ttl=61 time=77.8 ms--- docker.io ping statistics ---3 packets transmitted, 3 received, 0% packet loss, time 2003msrtt min/avg/max/mdev = 76.722/78.695/81.533/2.057 ms$ docker ps -l --no-truncCONTAINER ID IMAGE COMMAND CREATED0d739d5ea4e5 ping:latest "/bin/ping -c 3 docker.io" 51 seconds ago
運行docker鏡像, 感覺上和執行任何其他的程序沒有區別 --- 你指定要執行的程序(ping) 和 指定ping命令需要的參數.
注意到參數-c 3, 這個參數表示ping請求只發送3次, 這個參數包括在ENTRYPOINT裡面, 相當於硬編碼docker鏡像中. 每次執行docker鏡像都會帶上這個參數, 並且也不能被CMD參數覆蓋.
永遠使用Exec表示法
組合使用ENTRYPOINT和CMD命令式, 確保你一定用的是Exec表示法. 如果用其中一個用的是Shell表示法, 或者一個是Shell表示法, 另一個是Exec表示法, 你永遠得不到你預期的效果.
下表列出了如果把Shell表示法和Exec表示法混合, 最終得到的命令行, 可以看到如果有Shell表示法存在, 很難得到正確的效果:
Dockerfile CommandENTRYPOINT /bin/ping -c 3CMD localhost /bin/sh -c /bin/ping -c 3 /bin/sh -c localhostENTRYPOINT ["/bin/ping","-c","3"]CMD localhost /bin/ping -c 3 /bin/sh -c localhostENTRYPOINT /bin/ping -c 3CMD ["localhost"]" /bin/sh -c /bin/ping -c 3 localhostENTRYPOINT ["/bin/ping","-c","3"]CMD ["localhost"] /bin/ping -c 3 localhost
從上面看出, 只有ENTRYPOINT和CMD都用Exec表示法, 才能得到預期的效果
結論
如果你想讓你的docker image做真正的工作, 一定會在Dockerfile里用到ENTRYPOINT或是CMD. 但是請注意,這2個命令不是互斥的. 在很多情況下, 你可以組合ENTRYPOINT和CMD命令, 提升最終用戶的體驗.
推薦閱讀:
※如何提升Docker for Mac性能
※如何基於Docker進行開發?
※這張圖裡的幾個動物分別是指的哪些軟體項目?
※如何學習、了解kubernetes?
※如何評價 hyper_?
TAG:Docker |