Docker實踐之03-Dockerfile指令詳解

Dockerfile文件指令

目錄

FROM,指定基礎鏡像

FROM指令用於指定基礎鏡像。html

格式: FROM <基礎鏡像名稱> mysql

所謂定製鏡像,那必定是以一個鏡像爲基礎,在其上進行定製。就像咱們以前運行了一個nginx鏡像的容器,再進行修改同樣,基礎鏡像是必須指定的。而FROM就是指定基礎鏡像,所以一個Dockerfile中FROM是必備的指令,而且必須是第一條指令nginx

RUN,執行命令

RUN指令是用來執行命令行命令的。因爲命令行的強大能力,RUN指令在定製鏡像時是最經常使用的指令之一。其格式有兩種:git

  • shell格式

RUN <命令>
就像直接在命令行中輸入的命令同樣。如: RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html github

  • exec格式

RUN ["可執行文件", "參數1", "參數2"],這更像是函數調用中的格式。redis

有些命令適合用shell格式執行,有些命令適合用exec格式執行,靈活選擇便可。sql

COPY,複製文件

COPY命令也有兩種格式,一種相似於命令行,一種相似於函數調用。docker

  • shell格式

COPY [--chown= : ] <源路徑> ... <目標路徑> shell

  • exec格式

COPY [--chown= : ] [" <源路徑1> ",... " <目標路徑> "] 數據庫

COPY指令將構建上下文目錄中 <源路徑> 的文件/目錄複製到新的一層的鏡像內的 <目標路徑> 位置。好比:

COPY package.json /usr/src/app/

將構建上下文路徑中的package.json文件複製到新鏡像的/usr/src/app/路徑下。

<源路徑> 能夠是多個,甚至能夠是通配符,其通配符規則要知足Go的filepath.Match規則,如:

COPY hom* /mydir/
COPY hom?.txt /mydir/

<目標路徑> 能夠是容器內的絕對路徑,也能夠是相對於工做目錄的相對路徑(工做目錄能夠用WORKDIR指令來指定)。目標路徑不須要事先建立,若是目錄不存在會在複製文件前先行建立缺失目錄。
在使用該指令的時候還能夠加上 --chown=<user>:<group>選項來改變文件的所屬用戶及所屬組。
此外,還須要注意一點: 使用COPY指令,源文件的各類元數據都會保留。好比讀、寫、執行權限、文件變動時間等。這個特性對於鏡像定製頗有用。特別是構建相關文件都在使用Git進行管理的時候。

ADD,複製並解壓文件

ADD指令和COPY的格式和性質基本一致,可是在COPY基礎上增長了一些功能。

  • shell格式

ADD [--chown= : ] <源路徑> ... <目標路徑>

  • exec格式

ADD [--chown= : ] [" <源路徑1> ",..." <目標路徑> "]

若是 <源路徑> 爲一個tar壓縮文件,且壓縮格式爲gzip,bzip2以及xz的狀況下, ADD指令將會自動解壓縮這個壓縮文件到 <目標路徑> 去。
在使用該指令的時候還能夠加上 --chown=<user>:<group>選項來改變文件的所屬用戶及所屬組。
在Docker官方的Dockerfile最佳實踐文檔中要求,儘量的使用COPY,由於COPY的語義很明確,就是複製文件而已,而ADD則包含了更復雜的功能,其行爲也不必定很清晰。最適合使用ADD的場合,就是所說起的須要自動解壓縮的場合。
另外須要注意的是,ADD指令會令鏡像構建緩存失效,從而可能會令鏡像構建變得比較緩慢。

在COPY和ADD指令中選擇的時候,能夠遵循這樣的原則:全部的文件複製均使用COPY指令,僅在須要自動解壓縮的場合使用ADD。

CMD,容器啓動命令

CMD用於指定容器啓動後須要執行的命令,指令的格式和RUN類似,也是兩種格式:

  • shell格式

CMD <命令>

  • exec格式

CMD ["可執行文件", "參數1", "參數2"...]

以前介紹容器的時候曾經說過,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不是虛擬機,容器中的應用都應該之前臺執行,而不是像虛擬機/物理機裏面那樣,用upstart/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格式。

  • shell格式

ENTRYPOINT 命令

  • exec格式

ENTRYPOINT ["可執行文件", "參數1", "參數2"...]

ENTRYPOINT的目的和CMD同樣,都是指定容器啓動時執行的程序及參數。ENTRYPOINT在運行時也能夠替代,不過比CMD要略顯繁瑣,須要經過docker run的參數「--entrypoint」來指定。當指定了ENTRYPOINT後,CMD的含義就發生了改變,再也不是直接地運行其命令,而是將CMD的內容做爲參數傳給ENTRYPOINT指令,換句話說實際執行時,將變爲:<ENTRYPOINT> "<CMD指定的命令>"
那麼有了CMD後,爲何還要有ENTRYPOINT呢?這種<ENTRYPOINT> "<CMD>"有什麼好處麼?讓咱們來看幾個場景。

場景一:讓鏡像變成像命令同樣使用

本質上講,就是在啓動容器時,能夠給容器啓動後執行的命令指定參數。
舉個例子:一般使用CMD命令指定容器啓動後執行的命令,如:CMD ["curl", "-s", "https://ip.cn"],該命令用於獲取當前公網IP地址,若是但願顯示HTTP頭信息,實際上只須要爲curl命令添加「-i」參數便可,可是卻不能在容器啓動時直接指定-i參數,以下方式將會報錯:docker run imagename -i;可是,若是使用ENTRYPOINT指定容器啓動時執行的命令,則能夠直接在啓動容器時指定參數,即:ENTRYPOINT ["curl", "-s", "https://ip.cn"],運行容器:docker run imagename -i能夠顯示HTTP頭信息。

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

啓動容器就是啓動主進程,但有些時候啓動主進程前須要一些準備工做。
好比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)

在這裏使用id命令替代了CMD默認指定內容「redis-server」,故以root用戶沒法啓動redis服務。
而若是不明確指定容器啓動時執行的命令(此時CMD的內容爲默認值「redis-server」),或者明確指定CMD內容爲「redis-server」,則能夠正常啓動redis服務(使用redis用戶身份啓動)。

# 使用以下2種方式均可以正常啓動redis服務
## 方式1:啓動redis服務時不明確指定CMD內容,使用默認值「redis-server」
$ docker run -it redis

## 方式2:啓動redis服務時明確指定CMD內容爲「redis-server」
$ docker run -it redis redis-server

ENV,設置環境變量

格式有兩種:

  • ENV <key> <value>
  • ENV <key1>=<value1> <key2>=<value2>...

這個指令很簡單,就是設置環境變量而已,不管是後面的其它指令(如:RUN),仍是運行時的應用,均可以直接使用這裏定義的環境變量。

ENV VERSION=1.0 DEBUG=on NAME="Happy Feet"

這個例子中演示了對含有空格的值用雙引號括起來的辦法,這和Shell下的行爲是一致的。
定義了環境變量,那麼在後續的指令中,就可使用這個環境變量。引用環境變量時使用符號$,如:$VERSION

ARG,構建參數

格式: ARG <參數名>[=<默認值>]
ARG構建參數和ENV的效果同樣,都是設置環境變量。所不一樣的是,ARG所設置的是構建環境的環境變量,在未來容器運行時是不會存在這些環境變量的。可是不要所以就使用ARG保存密碼之類的信息,由於docker history仍是能夠看到全部值的。
Dockerfile中的ARG指令是定義參數名稱,以及定義其默認值。該默認值能夠在構建命令docker build中用--build-arg <參數名>=<值>來覆蓋。

VOLUME,定義匿名卷

格式:

  • VOLUME ["<路徑1>", "<路徑2>"...]
  • VOLUME <路徑>

以前咱們說過,容器運行時應該儘可能保持容器存儲層不發生寫操做,對於數據庫類須要保存動態數據的應用,其數據庫文件應該保存於卷(volume)中。爲了防止運行時用戶忘記將動態文件所保存目錄掛載爲卷,在Dockerfile中,咱們能夠事先指定某些目錄掛載爲匿名卷,這樣在運行時若是用戶不指定掛載,其應用也能夠正常運行,不會向容器存儲層寫入大量數據,如:

VOLUME /data

這裏的/data目錄就會在運行時自動掛載爲匿名卷,任何向/data中寫入的信息都不會記錄進容器存儲層,從而保證了容器存儲層的無狀態化。固然,運行時能夠覆蓋這個掛載設置。好比:
docker run -d -v mydata:/data xxxx
在這行命令中,就使用了mydata這個命名卷掛載到了/data這個位置,替代了Dockerfile中定義的匿名卷的掛載配置。

EXPOSE,聲明端口

格式爲:
EXPOSE <端口1> [<端口2>...]

EXPOSE指令是聲明運行時容器提供服務的端口,這只是一個聲明,在運行時並不會由於這個聲明應用就會開啓這個端口的服務。在Dockerfile中寫入這樣的聲明有兩個好處,一個是幫助鏡像使用者理解這個鏡像服務的守護端口,以方便配置映射;另外一個用處則是在運行時使用隨機端口映射時(即: docker run -P),會自動隨機映射EXPOSE的端口。

要將EXPOSE和在運行時使用-p <宿主端口>:<容器端口>區分開來。-p是映射宿主機端口和容器端口,換句話說,就是將容器的對應端口服務公開給外界訪問;而EXPOSE僅僅是聲明容器打算使用什麼端口而已,並不會自動在宿主進行端口映射。

WORKDIR,指定工做目錄

格式:
WORKDIR <工做目錄路徑>

使用WORKDIR指令能夠來指定工做目錄(或者稱爲當前目錄),之後各層的當前目錄就被改成指定的目錄,如該目錄不存在, WORKDIR會幫你創建目錄。以前提到一些初學者常犯的錯誤是把Dockerfile等同於Shell腳原本書寫,這種錯誤的理解還可能會致使出現下面這樣的錯誤:

RUN cd /app
RUN echo "hello" > world.txt

若是將這個Dockerfile進行構建鏡像運行後,會發現找不到/app/world.txt文件,或者其內容不是hello。緣由其實很簡單,在Shell中,連續兩行是同一個進程執行環境,所以前一個命令修改的內存狀態,會直接影響後一個命令;而在Dockerfile中,這兩行RUN命令的執行環境根本不一樣,是兩個徹底不一樣的容器。這就是對Dockerfile構建分層存儲的概念不瞭解所致使的錯誤。
以前說過每個RUN都是啓動一個容器、執行命令、而後提交存儲層文件變動。第一層RUN cd /app的執行僅僅是當前進程的工做目錄變動,一個內存上的變化而已,其結果不會形成任何文件變動。而到第二層的時候,啓動的是一個全新的容器,跟第一層的容器徹底不要緊,天然不可能繼承前一層構建過程當中的內存變化。
所以若是須要改變之後各層的工做目錄的位置,那麼應該使用WORKDIR指令。

USER,指定當前用戶

格式:
USER <用戶名>

USER指令和WORKDIR類似,都是改變環境狀態並影響之後的層。WORKDIR是改變工做目錄,USER則是改變以後層的執行RUN, CMD以及ENTRYPOINT這類命令的身份。固然,和WORKDIR同樣,USER只是幫助你切換到指定用戶而已,這個用戶必須是事先創建好的,不然沒法切換。

RUN groupadd -r redis && useradd -r -g redis redis
USER redis
RUN [ "redis-server" ]

若是以root執行的腳本,在執行期間但願改變身份,好比但願以某個已經創建好的用戶來運行某個服務進程,不要使用su或者sudo,這些都須要比較麻煩的配置,並且在TTY缺失的環境下常常出錯。建議使用gosu 。

# 創建 redis 用戶,並使用 gosu 換另外一個用戶執行命令
RUN groupadd -r redis && useradd -r -g redis redis
# 下載 gosu
RUN wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/1.7/gosu-amd64" && chmod +x /usr/local/bin/gosu && gosu nobody true
# 設置 CMD,並以另外的用戶執行
CMD [ "exec", "gosu", "redis", "redis-server" ]

HEALTHCHECK,健康檢查

格式:

  • HEALTHCHECK [選項] CMD <命令>:設置檢查容器健康情況的命令
  • HEALTHCHECK NONE:若是基礎鏡像有健康檢查指令,使用這行能夠屏蔽掉其健康檢查指令

HEALTHCHECK指令是告訴Docker應該如何進行判斷容器的狀態是否正常,這是Docker1.12引入的新指令。
在沒有HEALTHCHECK指令前,Docker引擎只能夠經過容器內主進程是否退出來判斷容器是否狀態異常。不少狀況下這沒問題,可是若是程序進入死鎖狀態,或者死循環狀態,應用進程並不退出,可是該容器已經沒法提供服務了。在1.12之前,Docker不會檢測到容器的這種狀態,從而不會從新調度,致使可能會有部分容器已經沒法提供服務了卻還在接受用戶請求。
自1.12以後,Docker提供了HEALTHCHECK指令,經過該指令指定一行命令,用這行命令來判斷容器主進程的服務狀態是否還正常,從而比較真實的反應容器實際狀態。當在一個鏡像指定了HEALTHCHECK指令後,用其啓動容器,初始狀態會爲starting ,在HEALTHCHECK指令檢查成功後變爲healthy,若是連續必定次數失敗,則會變爲unhealthy 。

HEALTHCHECK支持下列選項:
--interval= <間隔> :兩次健康檢查的間隔,默認爲30秒;
--timeout= <時長> :健康檢查命令運行超時時間,若是超過這個時間,本次健康檢查就被視爲失敗,默認30秒;
--retries= <次數> :當連續失敗指定次數後,則將容器狀態視爲unhealthy,默認3次。

CMD, ENTRYPOINT同樣,HEALTHCHECK只能夠出現一次,若是寫了多個,只有最後一個生效。
HEALTHCHECK [選項] CMD後面的命令,格式和ENTRYPOINT同樣,分爲shell格式和exec格式。命令的返回值決定了該次健康檢查的成功與否,0 :成功; 1 :失敗; 2 :保留,不要使用這個值。

ONBUILD,爲他人作嫁衣裳

格式:
ONBUILD <其它指令>

ONBUILD是一個特殊的指令,它後面跟的是其它指令,好比RUN,COPY等,而這些指令在當前鏡像構建時並不會被執行。只有當以當前鏡像爲基礎鏡像,去構建下一級鏡像的時候纔會被執行。
Dockerfile中的其它指令都是爲了定製當前鏡像而準備的,惟有ONBUILD是爲了幫助別人定製本身而準備的。
ONBUILD指令經常使用於構建基礎鏡像的Dockerfile中。

【參考】 https://docs.docker.com/engine/reference/builder/ Dockerfie官方文檔 https://docs.docker.com/engine/userguide/engimage/dockerfile_best-practices/ Dockerfile最佳實踐文檔 https://github.com/docker-library/docs Docker官方鏡像Dockerfile

相關文章
相關標籤/搜索