天天看點

Docker-DockerFile指令詳解

我們已經介紹了 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=<次數>:當連續失敗指定次數之後,則将容器狀态視為

    unhealthy

    ,預設為3次

在沒有

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中仍然可以看到,是以還是需要了解一下的!