CMD 容器啟動命令 & ENTRYPOINT 入口點 ——Dockerfile


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 可執行文件,並且要求以前台形式運行。比如:

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 來實現:

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 么? 

$ 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 這參數,我們就必須重新完整的輸入這個命令:

$ docker run myip curl -s https://ip.cn -i

  

顯然不是很好的解決方案,而使用 ENTRYPOINT 就可以解決這個問題。現在我們重新用 ENTRYPOINT 來實現這個鏡像:

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

$ 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 中就是這么做的:

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 腳本

#!/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 身份執行

比如:

$ docker run -it redis id
uid=0(root) gid=0(root) groups=0(root)

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM