CMD 容器啟動指令
Docker 不是虛拟機,容器中的應用都應該以前台執行,而不是像虛拟機、實體機裡面那樣,用
systemd
去啟動背景服務,容器内沒有背景服務的概念。
對于容器而言,其啟動程式就是容器應用程序,容器就是為了主程序而存在的,主程序退出,容器就失去了存在的意義,進而退出,其它輔助程序不是它需要關心的東西。
CMD
指令的格式和
RUN
相似,也是兩種格式:
-
格式:shell
CMD <指令>
-
格式:exec
CMD ["可執行檔案", "參數1", "參數2"...]
- 參數清單格式:
。在指定了CMD ["參數1", "參數2"...]
指令後,用ENTRYPOINT
指定具體的參數。CMD
Docker 不是虛拟機,容器就是程序。既然是程序,那麼在啟動容器的時候,需要指定所運作的程式及參數。
CMD
指令就是用于指定預設的容器主程序的啟動指令的。
在運作時可以指定新的指令來替代鏡像設定中的這個預設指令,
比如,
ubuntu
鏡像預設的
CMD
是
/bin/bash
,如果我們直接
docker run -it ubuntu
的話,會直接進入
bash
。
我們也可以在運作時指定運作别的指令,如
docker run -it ubuntu cat /etc/os-release
。這就是用
cat /etc/os-release
指令替換了預設的
/bin/bash
指令了,輸出了系統版本資訊。
在指令格式上,一般推薦使用
exec
格式,這類格式在解析時會被解析為 JSON 數組,是以一定要使用雙引号
"
,而不要使用單引号。
如果使用
shell
格式的話,實際的指令會被包裝為
sh -c
的參數的形式進行執行。比如:
CMD echo $HOME
在實際執行中,會将其變更為:
CMD [ "sh", "-c", "echo $HOME" ]
這就是為什麼我們可以使用環境變量的原因,因為這些環境變量會被 shell 進行解析處理。
提到
CMD
就不得不提容器中應用在前台執行和背景執行的問題。這是初學者常出現的一個混淆。
Docker 不是虛拟機,容器中的應用都應該以前台執行,而不是像虛拟機、實體機裡面那樣,用
systemd
去啟動背景服務,容器内沒有背景服務的概念。
一些初學者将
CMD
寫為:
CMD service nginx start
然後發現容器執行後就立即退出了。甚至在容器内去使用
systemctl
指令結果卻發現根本執行不了。
這就是因為沒有搞明白前台、背景的概念,沒有區分容器和虛拟機的差異,依舊在以傳統虛拟機的角度去了解容器。
對于容器而言,其啟動程式就是容器應用程序,容器就是為了主程序而存在的,主程序退出,容器就失去了存在的意義,進而退出,其它輔助程序不是它需要關心的東西。
而使用
service nginx start
指令,則是希望 upstart 來以背景守護程序形式啟動
nginx
服務。而剛才說了
CMD service nginx start
會被了解為
CMD [ "sh", "-c", "service nginx start"]
,是以主程序實際上是
sh
。那麼當
service nginx start
指令結束後,
sh
也就結束了,
sh
作為主程序退出了,自然就會令容器退出。
正确的做法是直接執行
nginx
可執行檔案,并且要求以前台形式運作。比如:
1 | |
ENTRYPOINT 入口點
ENTRYPOINT
的格式和
RUN
指令格式一樣,分為
exec
格式和
shell
格式。
ENTRYPOINT
的目的和
CMD
一樣,都是在指定容器啟動程式及參數。
ENTRYPOINT
在運作時也可以替代,不過比
CMD
要略顯繁瑣,需要通過
docker run
的參數
--entrypoint
來指定。
當指定了
ENTRYPOINT
後,
CMD
的含義就發生了改變,不再是直接的運作其指令,而是将
CMD
的内容作為參數傳給
ENTRYPOINT
指令,換句話說實際執行時,将變為:
<ENTRYPOINT> "<CMD>"
那麼有了
CMD
後,為什麼還要有
ENTRYPOINT
呢?這種
<ENTRYPOINT> "<CMD>"
有什麼好處麼?讓我們來看幾個場景。
場景一:讓鏡像變成像指令一樣使用
假設我們需要一個得知自己目前公網 IP 的鏡像,那麼可以先用
CMD
來實作:
1 2 3 4 5 | |
假如我們使用
docker build -t myip .
來建構鏡像的話,如果我們需要查詢目前公網 IP,隻需要執行:
假如我們使用
docker build -t myip .
來建構鏡像的話,如果我們需要查詢目前公網 IP,隻需要執行:
$ docker run myip
目前 IP:61.148.226.66 來自:北京市 聯通
嗯,這麼看起來好像可以直接把鏡像當做指令使用了,不過指令總有參數,如果我們希望加參數呢?比如從上面的
CMD
中可以看到實質的指令是
curl
,那麼如果我們希望顯示 HTTP 頭資訊,就需要加上
-i
參數。那麼我們可以直接加
-i
參數給
docker run myip
麼?
1 2 | |
可以看到可執行檔案找不到的報錯,
executable file not found
。之前我們說過,跟在鏡像名後面的是
command
,運作時會替換
CMD
的預設值。是以這裡的
-i
替換了原來的
CMD
,而不是添加在原來的
curl -s https://ip.cn
後面。而
-i
根本不是指令,是以自然找不到。
那麼如果我們希望加入
-i
這參數,我們就必須重新完整的輸入這個指令:
1 | |
顯然不是很好的解決方案,而使用
ENTRYPOINT
就可以解決這個問題。現在我們重新用
ENTRYPOINT
來實作這個鏡像:
1 2 3 4 5 | |
再來嘗試直接使用
docker run myip -i
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | |
可以看到,這次成功了。這是因為當存在
ENTRYPOINT
後,
CMD
的内容将會作為參數傳給
ENTRYPOINT
,而這裡
-i
就是新的
CMD
,是以會作為參數傳給
curl
,進而達到了我們預期的效果。
場景二:應用運作前的準備工作
啟動容器就是啟動主程序,但有些時候,啟動主程序前,需要一些準備工作。
比如
mysql
類的資料庫,可能需要一些資料庫配置、初始化的工作,這些工作要在最終的 mysql 伺服器運作之前解決。
此外,可能希望避免使用
root
使用者去啟動服務,進而提高安全性,而在啟動服務前還需要以
root
身份執行一些必要的準備工作,最後切換到服務使用者身份啟動服務。
或者除了服務外,其它指令依舊可以使用
root
身份執行,友善調試等。
這些準備工作是和容器
CMD
無關的,無論
CMD
為什麼,都需要事先進行一個預處理的工作。
這種情況下,可以寫一個腳本,然後放入
ENTRYPOINT
中去執行,而這個腳本會将接到的參數(也就是
<CMD>
)作為指令,在腳本最後執行。
比如官方鏡像
redis
中就是這麼做的:
1 2 3 4 5 6 7 8 | |
可以看到其中為了 redis 服務建立了 redis 使用者,并在最後指定了
ENTRYPOINT
為
docker-entrypoint.sh
腳本。
1 2 3 4 5 6 7 8 9 | |
該腳本的内容就是根據
CMD
的内容來判斷,如果是
redis-server
的話,則切換到
redis
使用者身份啟動伺服器,否則依舊使用
root
身份執行。
比如:
1 2 | |