Docker CMD 及 ENTRYPOINT,以及如何在 Docker 同時執行多個程序
在撰寫 Dockerfile 時,經常需要設定容器執行後的指令,例如,一個 Node.js 的 App 可能透過 npm run start
來啟動,又或是執行某個 shell script。
這些指令都是需要在環境準備好之後,在容器「運行」時執行,而非在 Docker build 的建置階段時執行。此時,我們便能在 Dockerfile 中使用 CMD
或 ENTRYPOINT
來指定容器運行時要執行的指令或程序。
CMD
及 ENTRYPOINT
CMD
用來提供容器運行時的預設指令,例如在 Dockerfile 內填入 CMD ["echo", "Hello World"]
,當我們執行 docker run
或 docker exec
時便會執行 echo Hello World
。
而 ENTRYPOINT
則是用來提供容器運行時的主執行程序,沒有特別指定時,預設為 /bin/sh -c
。若特別在 Dockerfile 內指定 ENTRYPOINT ["echo", "Hello World"]
的話,則在運行容器時會將 echo Hello World
當作主程序來執行。
雖然兩者都可以執行相同的指令,但是他們的行為卻有以下關鍵的差異:
覆蓋行為:
CMD
是預設指令,可以被docker run <image> <command>
可以被指定的<command>
覆蓋,而ENTRYPOINT
則無法被執行覆蓋,需要明確指定--entrypoint
參數才能修改主執行程序及參數傳遞:由於
ENTRYPOINT
定義了「主執行程序」,因此當我們設定ENTRYPOINT ["echo"]
時,在執行docker run <image> <args>
時其實會將<args>
附加在echo
的後面。所以執行docker run <image> "Hello World"
時實際會執行echo "Hello World"
簡單來說,CMD
提供容器運行的預設指令,而 ENTRYPOINT
則用來設定容器的主執行程序。
如何讓容器運行時可以同時執行多個程序
在官方的文件 Run multiple processes in a container 中提到,通常不建議在一個容器內執行多個服務程序,維持單一職責較佳。這邊指的 multiple processes 並非指一個 process fork 出來的多個 child processes,而是像 web server 這樣僅負責單一任務的 process (會 fork 多個 workers 來處理請求)。
然而,有些情況就不一定適合拆開來了,例如想在 Docker 中同時執行 Xvfb(虛擬螢幕)與 VNC server,讓遠端能透過 VNC 連進來查看,那麼就既要跑 Xvfb 的主程式,也要同時跑 VNC server。
例如,我們可以在 Dockerfile 中寫入
1 | ENTRYPOINT [ |
這段指令會用 shell 執行 Xvfb 和 X11vnc(並使用 &
在指令後方,將兩者放在背景執行),最後透過 exec "$@"
將當前 shell 的 process 替換成傳入的所有參數("$@"
)。也就是說,當我們執行 docker run <image> <command>
時,<command>
會替換掉目前執行的 Xvfb & ...
而成為最終的主程序。
雖然目前的指令還算簡潔,但若有更多服務或是參數的加入,將指令直接寫在 Dockerfile 裡就會讓 ENTRYPOINT
的內容變得相當冗長。
用 Shell Script 當 ENTRYPOINT
所以當我們要在啟動容器時執行多個主要程序時,可以將這些程式的啟動指令寫入一個 shell script 中,例如
start-vnc-services.sh
1 | Xvfb & |
如此一來,容器啟動時就能先在背景執行 Xvfb
和 x11vnc
。當我們執行 docker run <image> bash
時,原本 start-vnc-services.sh
的這個 script 是整個容器的主程式(PID 1),就會因為 exec "$@"
的關係,最終被傳入的 bash
指令所取代 PID1 的這個位置。