天天看點

docker CMD ENTRYPOINT差別

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

CMD [

"nginx"

"-g"

"daemon off;"

]

  

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

FROM ubuntu:18.04

RUN apt-get update \

&& apt-get 

install

-y curl \

&& 

rm

-rf 

/var/lib/apt/lists/

*

CMD [ 

"curl"

"-s"

"https://ip.cn"

]

 

假如我們使用 

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

$ docker run myip -i

docker: Error response from daemon: invalid header field value 

"oci runtime error: container_linux.go:247: starting container process caused \"exec: \\\"-i\\\": executable file not found in $PATH\"\n"

.

  可以看到可執行檔案找不到的報錯,

executable file not found

。之前我們說過,跟在鏡像名後面的是 

command

,運作時會替換 

CMD

 的預設值。是以這裡的 

-i

 替換了原來的 

CMD

,而不是添加在原來的 

curl -s https://ip.cn

 後面。而 

-i

 根本不是指令,是以自然找不到。

那麼如果我們希望加入 

-i

 這參數,我們就必須重新完整的輸入這個指令:

1

$ docker run myip curl -s https:

//ip

.cn -i

  

顯然不是很好的解決方案,而使用 

ENTRYPOINT

 就可以解決這個問題。現在我們重新用 

ENTRYPOINT

 來實作這個鏡像:

1

2

3

4

5

FROM ubuntu:18.04

RUN apt-get update \

&& apt-get 

install

-y curl \

&& 

rm

-rf 

/var/lib/apt/lists/

*

ENTRYPOINT [ 

"curl"

"-s"

"https://ip.cn"

]

再來嘗試直接使用 

docker run myip -i

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

$ docker run myip

目前 IP:61.148.226.66 來自:北京市 聯通

$ docker run myip -i

HTTP

/1

.1 200 OK

Server: nginx

/1

.8.0

Date: Tue, 22 Nov 2016 05:12:40 GMT

Content-Type: text

/html

; charset=UTF-8

Vary: Accept-Encoding

X-Powered-By: PHP

/5

.6.24-1~dotdeb+7.1

X-Cache: MISS from cache-2

X-Cache-Lookup: MISS from cache-2:80

X-Cache: MISS from proxy-2_6

Transfer-Encoding: chunked

Via: 1.1 cache-2:80, 1.1 proxy-2_6:8006

Connection: keep-alive

目前 IP:61.148.226.66 來自:北京市 聯通

可以看到,這次成功了。這是因為當存在 

ENTRYPOINT

 後,

CMD

 的内容将會作為參數傳給 

ENTRYPOINT

,而這裡 

-i

 就是新的 

CMD

,是以會作為參數傳給 

curl

,進而達到了我們預期的效果。

場景二:應用運作前的準備工作

啟動容器就是啟動主程序,但有些時候,啟動主程序前,需要一些準備工作。

比如 

mysql

 類的資料庫,可能需要一些資料庫配置、初始化的工作,這些工作要在最終的 mysql 伺服器運作之前解決。

此外,可能希望避免使用 

root

 使用者去啟動服務,進而提高安全性,而在啟動服務前還需要以 

root

 身份執行一些必要的準備工作,最後切換到服務使用者身份啟動服務。

或者除了服務外,其它指令依舊可以使用 

root

 身份執行,友善調試等。

這些準備工作是和容器 

CMD

 無關的,無論 

CMD

 為什麼,都需要事先進行一個預處理的工作。

這種情況下,可以寫一個腳本,然後放入 

ENTRYPOINT

 中去執行,而這個腳本會将接到的參數(也就是 

<CMD>

)作為指令,在腳本最後執行。

比如官方鏡像 

redis

 中就是這麼做的:

1

2

3

4

5

6

7

8

FROM alpine:3.4

...

RUN addgroup -S redis && adduser -S -G redis redis

...

ENTRYPOINT [

"docker-entrypoint.sh"

]

EXPOSE 6379

CMD [ 

"redis-server"

]

可以看到其中為了 redis 服務建立了 redis 使用者,并在最後指定了 

ENTRYPOINT

 為 

docker-entrypoint.sh

 腳本。

1

2

3

4

5

6

7

8

9

#!/bin/sh

...

# allow the container to be started with `--user`

if

"$1"

'redis-server'

-a 

"$(id -u)"

'0'

]; 

then

chown

-R redis .

exec

su

-

exec

redis 

"$0"

"$@"

fi

exec

"$@"

  該腳本的内容就是根據 

CMD

 的内容來判斷,如果是 

redis-server

 的話,則切換到 

redis

 使用者身份啟動伺服器,否則依舊使用 

root

 身份執行。

比如:

1

2

$ docker run -it redis 

id

uid=0(root) gid=0(root) 

groups

=0(root)

  

繼續閱讀