FROM
指令用於指定基礎鏡像。html
格式: FROM <基礎鏡像名稱> mysql
所謂定製鏡像,那必定是以一個鏡像爲基礎,在其上進行定製。就像咱們以前運行了一個nginx鏡像的容器,再進行修改同樣,基礎鏡像是必須指定的。而FROM就是指定基礎鏡像,所以一個Dockerfile中FROM是必備的指令,而且必須是第一條指令。nginx
RUN
指令是用來執行命令行命令的。因爲命令行的強大能力,RUN指令在定製鏡像時是最經常使用的指令之一。其格式有兩種:git
RUN <命令>
就像直接在命令行中輸入的命令同樣。如: RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
github
RUN ["可執行文件", "參數1", "參數2"],這更像是函數調用中的格式。redis
有些命令適合用shell格式執行,有些命令適合用exec格式執行,靈活選擇便可。sql
COPY
命令也有兩種格式,一種相似於命令行,一種相似於函數調用。docker
COPY [--chown=
COPY [--chown=
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
指令和COPY的格式和性質基本一致,可是在COPY基礎上增長了一些功能。
ADD [--chown=
ADD [--chown=
若是 <源路徑> 爲一個tar壓縮文件,且壓縮格式爲gzip,bzip2以及xz的狀況下, ADD指令將會自動解壓縮這個壓縮文件到 <目標路徑> 去。
在使用該指令的時候還能夠加上 --chown=<user>:<group>
選項來改變文件的所屬用戶及所屬組。
在Docker官方的Dockerfile最佳實踐文檔中要求,儘量的使用COPY,由於COPY的語義很明確,就是複製文件而已,而ADD則包含了更復雜的功能,其行爲也不必定很清晰。最適合使用ADD的場合,就是所說起的須要自動解壓縮的場合。
另外須要注意的是,ADD指令會令鏡像構建緩存失效,從而可能會令鏡像構建變得比較緩慢。
在COPY和ADD指令中選擇的時候,能夠遵循這樣的原則:全部的文件複製均使用COPY指令,僅在須要自動解壓縮的場合使用ADD。
CMD
用於指定容器啓動後須要執行的命令,指令的格式和RUN類似,也是兩種格式:
CMD <命令>
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的格式和RUN指令格式同樣,分爲exec格式和shell格式。
ENTRYPOINT 命令
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 <key> <value>
ENV <key1>=<value1> <key2>=<value2>...
這個指令很簡單,就是設置環境變量而已,不管是後面的其它指令(如:RUN
),仍是運行時的應用,均可以直接使用這裏定義的環境變量。
ENV VERSION=1.0 DEBUG=on NAME="Happy Feet"
這個例子中演示了對含有空格的值用雙引號括起來的辦法,這和Shell下的行爲是一致的。
定義了環境變量,那麼在後續的指令中,就可使用這個環境變量。引用環境變量時使用符號$
,如:$VERSION
。
格式: ARG <參數名>[=<默認值>]
ARG構建參數和ENV的效果同樣,都是設置環境變量。所不一樣的是,ARG所設置的是構建環境的環境變量,在未來容器運行時是不會存在這些環境變量的。可是不要所以就使用ARG保存密碼之類的信息,由於docker history
仍是能夠看到全部值的。
Dockerfile中的ARG指令是定義參數名稱,以及定義其默認值。該默認值能夠在構建命令docker build
中用--build-arg <參數名>=<值>
來覆蓋。
格式:
VOLUME ["<路徑1>", "<路徑2>"...]
VOLUME <路徑>
以前咱們說過,容器運行時應該儘可能保持容器存儲層不發生寫操做,對於數據庫類須要保存動態數據的應用,其數據庫文件應該保存於卷(volume)中。爲了防止運行時用戶忘記將動態文件所保存目錄掛載爲卷,在Dockerfile中,咱們能夠事先指定某些目錄掛載爲匿名卷,這樣在運行時若是用戶不指定掛載,其應用也能夠正常運行,不會向容器存儲層寫入大量數據,如:
VOLUME /data
這裏的/data目錄就會在運行時自動掛載爲匿名卷,任何向/data中寫入的信息都不會記錄進容器存儲層,從而保證了容器存儲層的無狀態化。固然,運行時能夠覆蓋這個掛載設置。好比:
docker run -d -v mydata:/data xxxx
在這行命令中,就使用了mydata這個命名卷掛載到了/data這個位置,替代了Dockerfile中定義的匿名卷的掛載配置。
格式爲:
EXPOSE <端口1> [<端口2>...]
EXPOSE指令是聲明運行時容器提供服務的端口,這只是一個聲明,在運行時並不會由於這個聲明應用就會開啓這個端口的服務。在Dockerfile中寫入這樣的聲明有兩個好處,一個是幫助鏡像使用者理解這個鏡像服務的守護端口,以方便配置映射;另外一個用處則是在運行時使用隨機端口映射時(即: docker run -P
),會自動隨機映射EXPOSE的端口。
要將EXPOSE和在運行時使用-p <宿主端口>:<容器端口>
區分開來。-p
是映射宿主機端口和容器端口,換句話說,就是將容器的對應端口服務公開給外界訪問;而EXPOSE僅僅是聲明容器打算使用什麼端口而已,並不會自動在宿主進行端口映射。
格式:
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指令和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 [選項] 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是一個特殊的指令,它後面跟的是其它指令,好比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