我們已經介紹了 FROM , RUN ,還提及了 COPY , ADD ,其實 Dockerfile 功能很強大,它提
供了十多個指令。下面我們繼續講解其他的指令
FROM
所謂定制鏡像,那麼就一定是以一個鏡像為基礎,在其上進行修改定制。就像我們之前運作了一個Nginx的容器,在其上面修改一樣,基礎容器是必需指定的。而
FROM
就是指定基礎鏡像,是以在DockerFile中,
FROM
是必備指定,并且必需是第一條指令!
除了指定現有的基礎鏡像以外,DockerFile還存在一個特殊的鏡像
srcatch
,這個鏡像是一個虛拟的概念,并不實際存在,它表示一個空白的鏡像:
FROM scratch
...
如果你以
scratch
作為基礎鏡像,意味着你将不使用任何鏡像為基礎,接下來你所寫的指令将作為第一層開始存在。不以任何系統為基礎,直接将可執行檔案複制進鏡像的做法并不罕見。如
swarm
、
coreos/etcd
。對Linux下靜态編譯的程式來說,并不需要其他操作提供其運作時支援,所需的一切庫都在可執行檔案裡了,是以使用
scratch
作為基礎,可以使鏡像的體積更加小巧。
RUN
RUN
指令是用來執行指令行指令的,由于指令行的強大功能,
RUN
指令是定制鏡像時最常用的指令之一。其格式有兩種:
- shell格式:就像在指令行中輸入的Shell腳本指令一樣,比如之前的:
- exec格式:像是函數調用的格式,例如:
apt-get update
mkdir -p /usr/src/redis
DockerFile的每一個指令都會新建構一層,
RUN
指令也不例外。每一個
RUN
行為,都會建立立一層,然後在其上執行指令,執行完畢後,送出這一層的修改,構成新的鏡像!
UnionFS是有最大層數限制的,比如AUFS,曾經是最大不能超過42層,現在是最大不能超過127層。是以,對于一些編譯、軟體的安裝、更新等操作,無需分成好幾層來操作,這樣會使得鏡像非常臃腫,擁有非常多的層,不僅僅增加了建構部署的時間,也很容易出錯!!例如,上面的
exec
格式的指令可以寫作一層:
這裡僅僅使用了一個
RUN
指令,是以隻會建立一層!對于一些編譯、安裝以及軟體的更新等操作,沒有必要分為很多層來操作,隻需要一層就可以了!在此,我們可以使用
&&
符号将多個指令分割開,使其先後執行。此時,一個
RUN
指令有可能會變得非常長,為了使DockerFile的可閱讀性和代碼更加美觀,我們可以使用
\
進行換行操作。另外,我們還可以使用
#
進行行首的注釋。
觀察剛剛編寫的
RUN
指令,我們會發現在指令的結尾處添加了清理工作的指令,删除了為了編譯建構的軟體,清理了所有下載下傳、展開的檔案,并且還清理
apt
緩存檔案。我們之前說過,鏡像是多層存儲,每一層存儲的東西不會在下一層删除,會一直跟随着鏡像。是以在鏡像建構時,一定要確定每一層隻添加真正需要的東西,任何無關的東西都應該被清理掉。
COPY
COPY
指令将從上下文目錄中的指定路徑下的檔案或檔案夾複制到新的一層的鏡像内的指定路徑之下,格式為:
COPY <源路徑> ... <目标路徑>
原路徑可以是多個,甚至是通配符,其通配規則隻需要滿足GO語言的
filepath.Math
規則即可,如下:
COPY ./test1.py ./test2.py /test/
COPY ./t*.py /test/
COPY ./test?.py /test/
目标路徑是容器内的絕對路徑,也可以是工作目錄下的相對路徑,工作目錄可以使用
WORKDIR
指令進行指定。目标路徑不需要事先建立,Docker會自動建立所需的檔案目錄。使用
COPY
指令會将源路徑的檔案的所有中繼資料,比如讀、寫、指定全選、時間變更等。如果源路徑時一個目錄,那麼會将整個目錄複制到容器中,包括檔案系統中繼資料。
ADD
ADD
指令和
COPY
的格式和性質基本一緻,隻不過是在
COPY
的基礎上增加了一些功能。例如
ADD
指定中,源路徑可以是一個遠端URL,Docker引擎會自動幫我們将遠端URL的檔案下載下傳下來到目标路徑下,例如:
ADD http://:/test.py /test/
我們使用
docker build
進行建構鏡像,然後使用
docker run
建立并啟動容器,會發現在根目錄下的test檔案夾下有了
test.py
檔案。如果源路徑是本地的一個tar壓縮檔案時,
ADD
指定在複制到目錄路徑下會自動将其進行解壓,如下:
ADD docker2.tar /test/
壓縮格式為
gzip
、
bzip2
以及
xz
的情況下,
ADD
指令都會将其解壓縮!
非常值得注意的是,目标路徑為一個URL時,會将其自動下載下傳到目标路徑下,但是其權限被自動設定成了
600
,如果這并不是你想要的權限,那麼你還需要額外增加一層
RUN
指令進行更改,另外,如果下載下傳的是一個壓縮包,同樣你還需要額外增加一層
RUN
指令進行解壓縮。是以,在這種情況下,你還不如指定隻用一層
RUN
,使用
curl
或者
wget
工具進行下載下傳,并更改權限,然後進行解壓縮,最後清理無用檔案!
當你的源路徑為壓縮檔案并且不想讓Docker引擎将其自動解壓縮,這個時候就不可以使用
ADD
指令,你可以使用
COPY
指令進行完成!
其實
ADD
指令并不實用,并不推薦使用!!!
CMD
CMD
指令與
RUN
指令相似,也具有兩種格式:
- shell格式:CMD <指令>
- exec格式:CMD [“可執行檔案”, “參數1”, “參數2”, …]
之前介紹容器的時候就說過,Docker不是虛拟機,容器就是程序。既然是程序,那麼在啟動容器的時候,就需要指定運作的程式及參數。
CMD
就是指定預設的容器主程序的啟動指令的。
在運作時可以設定
CMD
指令來代替鏡像設定中的指令,例如Ubuntu預設的
CMD
是
/bin/bash
,當我們使用指令
docker run -it ubuntu
建立并啟動一個容器會直接進入
bash
。我們也可以在運作時指定運作别的指令,比如
docker run -it ubuntu cat /etc/os-release
,這就用
cat /etc/os-release
指令代替了預設的
/bin/bash
指令,輸出了系統版本資訊。比如,我想在啟動容器的時候,在控制台中輸出
Hello Docker!
,我們可以在Dockerfile中這樣寫,如下:
FROM ubuntu
CMD echo "Hello Docker!"
接下來,我們建構一個鏡像
ubuntu:v1.0
,接下來,我們以此鏡像為基礎建立并啟動一個容器,如下:
docker run -it ubuntu:v1
這樣,就會在控制台中輸出
Hello Docker!
的資訊。
值得注意的是,如果使用shell格式,那麼實際的指令會被包裝成為
sh -c
的參數的形式進行執行。上面的
CMD
指令,在實際執行中會變成:
CMD ["sh", "-c", "echo", "Hello Docker!"]
因為這種特性,一些指令在加上
sh -c
之後,有可能會發生意想不到的錯誤,是以在Dockerfile中使用
RUN
指令時,更加推薦使用
exec
格式!最後需要牢記,使用
docker run
指令指定要執行的指令可以覆寫
RUN
指令,如果我們的
docker run
中指定了我們将要執行的指令,并且在Dockerfile中也指定了CMD指令,那麼最終隻會執行
docker run
指令中指定的指令。比如有這樣一個Dockerfile:
FROM ubuntu
CMD ["echo", "Hello Docker!"]
我們将其建構成成鏡像
ubuntu:v1.1
,下面,我們以此鏡像為基礎建立并啟動一個容器,如下:
docker run -it ubuntu:v1 cat /etc/os-release
那麼容器隻會執行
cat /etc/os-release
指令,也就是說在控制台隻會輸出系統版本資訊,并不會輸出Hello Docker!資訊
ENTRYPOINT
ENTRYPOINT
指令和
CMD
指令目的一樣,都是指定容器運作程式及參數,并且與
CMD
一樣擁有兩種格式的寫法:
- shell格式:ENTRYPOINT <指令>
- exec格式:ENTRYPOINT [“可執行檔案”, “參數1”, “參數2”, …]
與
CMD
指令一樣,
ENTRYPOINT
也更加推薦使用
exec
格式,
ENTRYPOINT
在
docker run
指令中同樣也可以進行指定,隻不過比
CMD
指令來的繁瑣一些,需要指定
--entrypoint
參數。同樣,在
docker run
指令中指定了
--entrypoint
參數的話,會覆寫Dockerfile中
ENTRYPOINT
上的指令。
當指定了
ENTRYPOINT
指令時,
CMD
指令裡的指令性質将會發生改變!
CMD
指令中的内容将會以參數形式傳遞給
ENTRYPOINT
指令中的指令,如下:
FROM ubuntu
ENTRYPOINT ["rm", "docker2"]
CMD ["-rf"]
其實,它真正執行的指令将會是:
從例子中可以看出,
ENTRYPOINT
指令和
CMD
指令非常的相似,也很容易将其搞混,就比如上面的例子,就可以完全使用一條
CMD
指令
CMD ["rm", "docker2", "-rf"]
來完成。這兩個指令到底有什麼差別,為什麼要同時保留這兩條指令呢?
我們可以使用
ENTRYPOINT
指令和
CMD
指令相結合,使得在建立并啟動時要執行的指令更加靈活!有如下Dockerfile:
FROM ubuntu
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
ENTRYPOINT ["curl", "-s", "http://ip.cn"]
此時,我們将其建構成鏡像
ubuntu:v1.2
,下面我們建立并啟動容器:
docker run -it ubuntu:v1
将會在控制台輸出我們相應的公網IP資訊!此時,如果我們還需要擷取HTTP頭資訊時,我們可以這樣:
docker run -it ubuntu:v1 -i
此時,将會在控制台中将公網IP資訊以及HTTP頭資訊全部輸出!我們知道,
docker run
指令中緊跟在鏡像後面的是
CMD
指令指令,運作時會替換預設的
CMD
指令。因為我們在Dockerfile中指定了
ENTRYPOINT
指令,根據
ENTRYPOINT
指令的特性知道,當指定了
ENTRYPOINT
指令,
CMD
指令的内容将會以參數的形式傳遞給
ENTRYPOINT
,是以在容器中最終執行的指令是
curl -s -i http://ip.cn
,
-i
參數被傳遞到
ENTRYPOINT
中,是以最終在控制台中會輸出HTTP頭資訊!!!
ENV
ENV
指令用于設定環境變量,格式有兩種:
- ENV
- ENV = = …
這個指令非常簡單,就是用于設定環境變量而已,無論是接下來的指令,還是在容器中運作的程式,都可以使用這裡定義的環境變量。例如:
FROM ubuntu:
ENV MODE=test
RUN apt-get update && apt-get install -y curl && curl http://192.168.0.89:5000/$MODE && rm -rf /var/lib/apt/lists/*
如果你要設定多個環境變量,為了美觀,你可以使用
\
來進行換行。多個環境變量的隔開,使用空格進行隔開的,如果某個環境變量的值是由一組英文單詞構成,那麼你可以将其使用
""
進行圈起來。如下:
FROM ubuntu:
RUN MODE=test DESCRITPION="ios 12" \
TITLE="iphone"
接下來,将這個Dockerfile建構成鏡像,然後以此鏡像為基礎建立并啟動一個容器,在容器中,我們調用這個環境變量,仍然是有用的!!!
值得注意的是,如果你想通過
CMD
或者
ENTRYPOINT
指令的exec格式來列印環境,就像下面這樣:
CMD ["echo", $MODE]
CMD ["echo", "$MODE"]
這樣都是不能正确輸出環境變量的值的,你可以改成exec格式來執行shell指令,如下:
CMD ["sh", "-c", "echo $MODE"]
如此,就能正确輸出環境變量的值了!
ARG
建構參數
ARG
和
ENV
指令一樣,都是設定環境變量。與之不同的是,
ARG
設定的環境變量隻是在鏡像建構時所設定的,在将來容器運作時是不會存在這些環境變量的。但是不要是以就用
ARG
來儲存密碼之類的資訊,因為通過
docker history
還是能夠看得到的。
ARG
指令與
ENV
指令的使用類似,如下:
FROM ubuntu:
ARG app="python-pip"
RUN apt-get update && apt-get install -y $app && rm -rf /var/lib/apt/lists/*
ARG
建構參數可以通過
docker run
指令中的
--build-arg
參數來進行覆寫
VOLUME
VOLUME
指令用于建構鏡像時定義匿名卷,其格式有兩種:
- VOLUME <路徑>
- VOLUME [“<路徑1>”, “<路徑2>”, …]
之前我們說過,容器存儲層應該保持無狀态化,容器運作時應盡量保持容器内不發生任何寫入操作,對于需要儲存動态資料的應用,其資料檔案應該将其儲存在資料卷中(VOLUME)
定義一個匿名卷:
FROM ubuntu:
VOLUME /data
定義多個匿名卷:
FROM ubuntu:
VOLUME ["/data", "/command"]
這裡的
/data
和
/command
目錄在容器運作時會自動挂載為匿名卷,任何向
/data
和
/command
目錄中寫入的資訊都不會記錄進容器存儲層,進而保證了容器存儲層的無狀态化!容器匿名卷目錄指定可以通過
docker run
指令中指定
-v
參數來進行覆寫
EXPOSE
EXPOSE
指令是聲明運作時容器服務端口,這隻是一個聲明,在運作時并不會因為這個聲明應用就會開啟這個端口的服務。在Dockerfile中這樣聲明有兩個好處:一個是幫助鏡像使用者更好的了解這個鏡像服務的守護端口,另一個作用則是在運作時使用随機端口映射時,也就是
docker run -p
指令時,會自動随機映射
EXPOSE
端口。
要将
EXPOSE
和在運作時使用
-p <宿主>:<容器端口>
區分開來,
-p
是映射宿主端口和容器端口,換句話說,就是将容器的對應端口服務公開給外界通路,而
EXPOSE
僅僅是聲明端口使用什麼端口而已,并不會自動在宿主進行端口映射。
WORKDIR
使用
WORKDIR
指令來制定工作目錄(或者稱為目前目錄),以後各層操作的目前目錄就是為指定的目錄,如果該目錄不存在,
WORKDIR
會自動幫你建立目錄,如下:
FROM ubuntu:
WORKDIR /data/test
RUN mkdir docker && echo "test" > demo.txt
當我們使用
docker build
建構此鏡像,并使用
docker run
指令進行建立和啟動容器之後,會發現目錄被自動切換到了
/data/test
,并且在目前目錄下有一個檔案夾
docker
,在
docker
下有一個檔案
domo.txt
并且有相應的内容。我們還可以為特定的指令指定不同的工作目錄,如下:
FROM ubuntu:
WORKDIR /data/test
RUN mkdir docker
WORKDIR /data/test/docker
RUN echo "test" > demo.txt
這樣,Dockerfile中兩次
RUN
指令的操作都在不同的目錄下進行,最終容器會切換到最後一次
WORKDIR
指令下的目錄。
WORKDIR
指令可以通過
docker run
指令中的
-w
參數來進行覆寫
USER
USER
指令用于将會用以什麼樣的使用者去運作,例如:
FROM ubuntu:
USER docker
基于該鏡像啟動的容器會以
docker
使用者的身份來運作,我們可以指定使用者名或者UID,組名或者GID,或者兩者的結合,如下:
FROM ubuntu:16.04
USER user
USER user:group
USER uid
USER uid:gid
USER user:gid
USER uid:group
USER
指令可以在
docker run
指令中的
-u
參數進行覆寫
HEALTHCHECK
HEALTHECHECK
指令是告訴Docker該如何判斷容器的狀态是否正常,這是1.12引入的新指令,其格式有兩種:
- HEALTHCHECK [options] CMD <指令>:檢查容器健康狀态的指令
- HEALTHCHECK NONE:如果基礎鏡像有健康檢查指令,這一行将會屏蔽掉其健康檢查指令
HEALTHECHECK支援下列選項:
- –interval=<間隔>:兩次檢查的時間間隔,預設為30s
- –timeout=<時長>:健康檢查指令運作逾時時間,如果超過這個時間,本次健康檢查将會判定為失敗,預設為30s
- –retries=<次數>:當連續失敗指定次數之後,則将容器狀态視為
,預設為3次unhealthy
在沒有
HEALTHCHECK
指令之前,Docker引擎隻可以通過容器内主程序是否退出來判斷容器狀态是否異常。很多情況下這沒有問題,但是如果程式進入了死鎖狀态,或者死循環狀态,應用程序并不退出,但是該容器已經無法繼續提供服務了。在1.12之前,Docker引擎不會檢測到容器的這種狀态,進而不會重新排程,導緻可能容器已經無法提供服務了卻仍然還在接收使用者的請求。
假設我們有個鏡像是最簡單的Web服務,我們希望增加健康檢查來判斷Web服務是否在正常工作,我們可以用
curl
來幫助判斷,其
Dockerfile
的
HEALTHCHECK
可以這麼寫:
FROM nginx
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
HEALTHCHECK --interval=5s --timeout=3s CMD curl -fs http://localhost/ || exit 1
接下來,我們将該Dockerfile編譯建構成一個鏡像,并以此鏡像為基礎建立并啟動一個容器。此時,我們使用
docker container ls
指令來檢視容器的狀态,如下:
root@ubuntu:~/docker# docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b91eea00d nginx:v1. "nginx -g 'daemon of…" seconds ago Up seconds (healthy) ...:->/tcp web
我們再
STATUS
這一列中可以看到,狀态未
healthy
。如果我們快速的多次執行
docker container ls
的話,會發現
STATUS
狀态是由
health: starting
最後變為
healthy
,當然如果容器未在正常工作,最後的狀态将會變為
unhealthy
這裡,我們設定了每5s檢查一次,如果檢查時間超過3s沒有響應就視為失敗。
||
符号左邊的指令執行結果為假,右邊的指令才會執行!
為了幫助排除故障,健康檢查指令的輸出會被存儲于健康狀态裡,我們可以使用
docker inspect
指令來進行檢視:
[email protected]:~/docker# docker inspect --format '{{json .State.Health}}' web | python3 -m json.tool
{
"Status": "healthy",
"FailingStreak": 0,
"Log": [
{
"Start": "2018-07-17T21:15:05.900643297+08:00",
"End": "2018-07-17T21:15:05.968989028+08:00",
"ExitCode": 0,
"Output": "<!DOCTYPE html>\n<html>\n<head>\n<title>Welcome to nginx!</title>\n<style>\n body {\n width: em;\n margin: auto;\n font-family: Tahoma, Verdana, Arial, sans-serif;\n }\n</style>\n</head>\n<body>\n<h1>Welcome to nginx!</h1>\n<p>If you see this page, the nginx web server is successfully installed and\nworking. Further configuration is required.</p>\n\n<p>For online documentation and support please refer to\n<a href=\"http://nginx.org/\">nginx.org</a>.<br/>\nCommercial support is available at\n<a href=\"http://nginx.com/\">nginx.com</a>.</p>\n\n<p><em>Thank you for using nginx.</em></p>\n</body>\n</html>\n"
}
]
}
和
CMD
、
NETRYPOINT
一樣,
HEALTHCHECK
指令隻可以出現一次,如果有多個
HEALTHCHECK
指令,那麼隻有最後一個才會生效!!!
ONBUILD
ONBUILD
是一個特殊的指令,它後面跟着的是其他指令,比如
COPY
、
RUN
等,而這些指令在目前鏡像被建構時,并不會被執行。隻有以目前鏡像為基礎鏡像去建構下一級鏡像時,才會被執行。格式為:
ONBUILD <其他指令>
Dockerfile
中的其他指令都是為了建構目前鏡像準備的,隻有
ONBUILD
指令是為了幫助别人定制而準備的。例如:
from ubuntu:
WORKDIR /data
ONBUILD RUN mkdir test
此時,我們以此
Dockerfile
進行建構鏡像
ubuntu:test
,并以此鏡像為基礎建立并啟動一個容器,進入容器後,容器會自動切換到
WORKDIR
指令下的目錄,此時我們使用
ls
指令會發現在工作目錄下,并未建立
test
檔案夾,如下:
root@ubuntu:~/docker# docker run -it ubuntu:test
root@3a8f912fd23b:/data# ls
root@3a8f912fd23b:/data#
此時,我們再建立一個
Dockerfile
,隻需一個
FROM
指令即可,使其繼承剛剛我們建構的
ubuntu:test
鏡像,如下:
FROM ubuntu:test
我們再以此
Dockerfile
建構鏡像
ubuntu:test_onbuild
,并以此鏡像為基礎建立并啟動一個容器,進入容器後,容器會自動切換到
WORKDIR
指令下的目錄,此時我們使用
ls
指令會發現在工作目錄下,已經建立好了一個名為
test
的檔案夾,如下:
root@ubuntu:~/docker# docker run -it ubuntu:test_onbuild
root@5394e605b6ea:/data# ls
test
LABEL
LABEL
指令可以為鏡像指定标簽,其格式為:
LABEL <key1>=<value1> <key2>=<value2> ...
LABEL
後面是鍵值對,多個鍵值對以空格進行隔開,如果value中包含空格,請使用
""
将value進行圈起來,如下:
FROM ubuntu:
LABEL name=test
LABEL description="a container is used to test"
我們知道,DockerFile的每一個指令都會新建構一層,是以,上面的
LABEL
我們可以寫成一條指令,用空格進行隔開,如下:
FROM ubuntu:
LABEL name=test description="a container is used to test"
為了美觀,我們還可以使用
\
符号進行換行操作。
要檢視鏡像的标簽,我們可以使用
docker inspect
指令,如下:
root@ubuntu:~# docker inspect --format '{{json .Config.Labels}}' test | python3 -m json.tool
{
"description": "a container is used to test",
"name": "test"
}
其中“test”為容器名稱!
值得注意的是,這裡的标簽并非是我們一開始将鏡像名稱中的
<倉庫>:<标簽>
,這兩者是不一樣的!這裡标簽,類似于簽條,注解之類的意思
MAINTAINER
MAINTAINER
指令用于指定生成鏡像的作者名稱,其格式為:
MAINTAINER <name>
MAINTAINER
指令已經被棄用,可以使用
LABEL
指令進行替代,如下:
MAINTAINER
指令在一些老的Dockerfile中仍然可以看到,是以還是需要了解一下的!