2017年2月15日

Dockerfile 初步閱讀與學習紀錄

最近花了點時間閱讀 Docker 官網的上 Dockerfile reference 這篇教學文件。閱讀後在加上一些自己實作測試,稍微有點心得,因此就想說再寫篇文章當作學習筆記吧。

Dockerfile 簡介

在 docker 要自己建置 image,你可以使用比較麻煩點的方法,就是自己先把自己想要的 base image 從 docker hub 拉下來,然後用 container 執行它,接著在 container 內執行你想做的命令,然後在 commit container 讓他變成 image,這方式就會變成是一直在重複用 container run,然後 commit 成 image 的動作。

docker 提供了另一種方式來建置 image,也就是本篇要提到的 Dockerfile。docker 可以透過 Dockerfile 讀指令的方式自動建立 image,也就是說你可以把你要 build image 的步驟透過 Dockerfile 的指令一步一步的寫出來,這樣就能使用 docker build 指令來幫你自動建置 image。至於中間過程需要用 container 執行你的命令、再將 container commit 回 image 這些瑣碎的細節,你可以不需要太在意他。

Context:

在開始寫 Dockerfile 之前,要先知道 build context 的概念,context 可以是本地端的目錄或是某個 URL(Git repository),context 不論是本地端目錄或是 URL,其底下的所有子目錄或是檔案都是屬於 context 內。

當你在 command line 下 docker build 指令要透過 Dockerfile 來 build image 時,docker 並不會在 command line 直接幫你 build,而是要透過 docker daemon 來執行。它會把 build context 傳送給 docker daemon,之後 docker daemon 在幫你 build。

所以 build 的一切過程,是在 docker daemon 上執行的,你在 build 過程中如果要複製檔案到 image 內,檔案也必須要先放在 context 內,這樣 docker daemon 才能在 build 過程中找到檔案。

另外你也可以把 build image 相關的說明文件放在 context 目錄內,然後在搭配 .dockerignore 檔案讓說明文件在 docker build 時不會被傳送到 docker daemon 中。

這邊的 context 有點像是在寫 project 時,一個 project 會放在某個目錄底下,該目錄可以視為一個 context,然後該 project 要執行所需的原始碼、lib、以及相關的說明文件都會放在此目錄底下的。

Dockerfile command:

Dockerfile command 說多不多,以下列出並加上簡短說明。

  • FROM:base image 設定,你在 build image 時,一定需要指定一個 image 來源。
  • RUN:build 時使用。會執行你的命令,然後 commit 結果,被 commit 的結果會被 Dockerfile 下一步驟所使用。
  • CMD:run container 時要執行的命令,要注意這個跟 RUN 的區別,他是指當你使用 docker run 時,container 建立起來的時候要跑什麼樣的命令。另外一個 Dockerfile 內要有一個 CMD 或是等等會提到的 ENTRYPOINT。
  • LABEL:標籤,會是以 key=value 的形式來定義。你可以用 LABEL 記錄一些 image 資訊,如版號、作者等等的訊息。官方建議用一個 LABEL 指令來定義多組值會比較有效率。
  • EXPOSE:告訴 container 在執行時需要監聽哪個 port。注意,EXPOSE 並不是直接開放 port 給外界訪問,要讓外界訪問,你要在下 docker run 指令時加上 -p flag 才能讓外界訪問。
  • ENV:環境變數設定。
  • ADD:將檔案加到 images 內,檔案來源可以是 URL 或是 context 內的檔案。
  • COPY:將檔案加到 images 內,檔案來源是 context 內的檔案,注意這個跟 ADD 的區別,上網查有人是說這個語意清楚,行為單純,就是複製檔案的概念,建議用這個。
  • ENTRYPOINT:run container 時要執行的命令,可以和 CMD 混搭用,此時 CMD 會被視為初始參數來使用。
  • VOLUME:掛載目錄用。
  • USER:可以指令使用者為何。在此指令過後的 Dockerfile 指令,都會用這個使用者來執行。
  • WORKDIR:工作目錄設定,在執行此指令後,在 Dockerfile 內後續的 RUN、CMD、ENTRYPOINT、COPY
  • ARG:buid 時可以設定參數,讓 docker build 指令可以下參數來處理。
  • ONBUILD:提供一個 trigger 機制,當別人要使用你 build 好的 image 當成 base 時,如果你當初在 build 時有寫 ONBUILD 指令,則別人在用這個 image 當成 base image 時,當跑完他 Dockerfile 的 FROM 指令之後會先跑你寫的 ONBUILD,接著才會開始跑它寫的其他指令。
  • STOPSIGNAL:要不要在結束時收到系統發的信號。
  • HEALTHCHECK:提供一個檢查的機制,你可以透過 HEALTHCHECK 來讓 docker 知道什麼樣的狀況對你的 container 來說才算是正常的。
  • SHELL:改變預設的 shell。Dockerfile command 會有 shell 格式的命令,預設是用 /bin/sh -c 來執行命令,你可以透過 SHELL 強制修改掉。

一個簡單的 Dockerfile 範例

接下來示範怎麼寫一個簡單的 Dockerfile:

  1. 首先先在本機上決定你的 context 要在哪,比如說我用以下路徑的 foobar 目錄,foobar 底下的所有檔案都會視為 context。

    /Users/mayer/Documents/dockerspace/foobar
  2. 由於我是使用 Mac 系統,所以我要用 docker 啟動時一併開啟的終端機來 build,先將終端機切換到該目錄:

    cd /Users/mayer/Documents/dockerspace/foobar
  3. 之後,透過 vi 新增 Dockerfile

    vi Dockerfile
  4. 然後開始撰寫 Dockerfile,這邊 FROM image 我們使用 alpine linux,這是個容量只有 3.9 MB 左右的超輕量 Linux,然後預設他並不會執行任何的命令,所以我們會讓他預設起來就執行 top 指令,Dockerfile 內容如下:

    FROM alpine:3.5
    CMD ["top"]
  5. 之後使用 :wq 離開 vi,這樣 Dockerfile 檔案就準備好了。

  6. 接著要使用此 Dockerfile 來 build 我們自己的 image,我們可以使用 docker build 指令來處理,以下是 build 的資訊:

    $ docker build -t mayer/foo:1.0.0 .
    Sending build context to Docker daemon 2.048 kB
    Step 1 : FROM alpine:3.5
    3.5: Pulling from library/alpine
    0a8490d0dfd3: Pull complete 
    Digest: sha256:dfbd4a3a8ebca874ebd2474f044a0b33600d4523d03b0df76e5c5986cb02d7e8
    Status: Downloaded newer image for alpine:3.5
     ---> 88e169ea8f46
    Step 2 : CMD top
     ---> Running in 5f41dcb5519c
     ---> 7265d7f11cd1
    Removing intermediate container 5f41dcb5519c
    Successfully built 7265d7f11cd1

    上述的 docker build 指令有用上 -t flag,用來指定 image 名稱與 tag,另外後面的 . 代表 context 的路徑為當下目錄。

    下完指令後的第一個訊息,可以看到 docker 先把 build context 傳送給 Docker daemon,然後才開始 build 步驟,首先 docker daemon 在執行 FROM 指令時發現到本地端沒有 alpine:3.5 這個 image,所以他先幫你 pull 下來,然後結束第一步驟。接著執行第二步驟 CMD top,指定這個 image 在透過 container 啟動時預設要執行的命令為 top,這樣就完成了 build image 步驟。

  7. 可以透過 docker images 查看是否 build 成功:

    $ docker images
    REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
    mayer/foo           1.0.0               7265d7f11cd1        8 minutes ago       3.984 MB
  8. 接著使用 container 來執行這個 image,這邊用 -d flag 讓他在背景執行:

    docker run -d mayer/foo:1.0.0
  9. 你可以透過 docker ps 來查詢 container 運行狀態:

    $ docker ps
    CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
    8332ee2f38a6        mayer/foo:1.0.0     "top"               30 seconds ago      Up 29 seconds                           nauseous_albattani      
  10. 另外除了 docker ps 指令之外,你也可以 Kilematic 工具來直接觀看 container 運作的內容,看畫面在跑感受會比較深一點。

透過以上步驟,我們已經完成了透過 Dockerfile 來 build image,然後使用 container 來執行 image 的流程了。

接著下面要講記錄一些讓人有些困惑的指令。

ENTRYPOINT 與 CMD

假設有個 Dockerfile 長這樣:

FROM centos:7.2.1511
ENTRYPOINT ["top", "-b"]
CMD ["-c"]

我們透過此 Dockerfile 來 build images:

docker build -t mayer/foo .

接著執行兩個 container:

docker run -d mayer/foo
docker run -d mayer/foo -H

使用 docker ps -a 查詢:

CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
83b6433d168b        mayer/foo           "top -b -H"         20 minutes ago      Up 4 minutes                            modest_hodgkin
20ba3187d8e3        mayer/foo           "top -b -c"         21 minutes ago      Up 9 seconds                            angry_bardeen

可以看到 Docker 會執行 ENTRYPOINT,若有 CMD,他會將它當成預設參數,你可以在 run 時改變它。

另外可以下指令查 images 相關資訊:

docker inspect mayer/foo:latest

他會印出此 image 預設的 Entrypoint 與 Cmd 為何:

    ...
    "Cmd": [
        "-c"
    ],
    "Image": "sha256:97aa54a9a5ebfeff5a2b1992a1e0cb0721162fc15061da2c4192205ba93f8554",
    "Volumes": null,
    "WorkingDir": "",
    "Entrypoint": [
        "top",
        "-b"
    ],
    ...

WORKDIR:

image 的 workdir 可以用 docker inspect 來觀察,比如說 tomcat:8.5.11-jre8 的:

$ docker inspect tomcat:8.5.11-jre8

...
"WorkingDir": "/usr/local/tomcat",
...

可以看到此 image,他把 WorkingDir 設定在 /usr/local/tomcat。然後我們想從這個 image 加上自己的 webapp,我們該如何做?

首先,在本機上建立一個目錄,把這個目錄視為 context,這邊先用以下路徑的 sample 目錄作為 context:

/Users/mayer/Documents/dockerspace/sample

然後用終端機切換到那個目錄內:

cd /Users/mayer/Documents/dockerspace/sample

然後我們要將 Web Application 放到這目錄,

mv /somepath/sample.war ./

接著使用 vi 來編輯檔案

vi Dockerfile

Dockerfile,內容如下:

FROM tomcat:8.5.11-jre8
COPY sample.war webapps/

可以注意到,這邊 COPY 指令的 dest,是直接寫 webapps/,這邊能這樣寫,是因為他有設定了 WORKDIR 的緣故,其實真正的 dest 會是:

/usr/local/tomcat/webapps

所以上面的 Dockerfile 是說,我要 build 一個 images,來源是 tomcat:8.5.11-jre8,然後幫我把檔案 sample.war 複製到 /usr/local/tomcat/webapps 目錄底下,這樣一來我就可以直接使用 docker build 來建立一個裡面包含我 webapp 的 tomcat image 了:

docker build -t mayer/sample .

注意 docker build 最後那個 . 代表 context 為當下目錄,不能省略。

CMD 與 EXPOSE

接著要如何執行呢?在執行之前,讓我們再看一下 tomcat:8.5.11-jre8 的詳細資訊

$ docker inspect tomcat:8.5.11-jre8

...
"ExposedPorts": {
            "8080/tcp": {}
        },
...
"Cmd": [
    "catalina.sh",
    "run"
],
...
"Entrypoint": null,
...

這邊要注意的是這三個資訊,一個是預設執行的 container 會監聽 8080,一個是他的初始 Cmd 就是讓 tomcat 啟動,然後他沒有寫 Entrypoint,所以可以不用擔心會影響到 Cmd,也就是說如果我們要使用我們 build 的 image 的話,我們只需要使用 docker run 指令來啟動 container 並執行,如下:

docker run -d -p 8080:8080 mayer/foo

注意 port 要透過 -p flag 處理,冒號前面的 8080 代表本地端機器的 port,冒號後面的 8080 代表 container 的 port。

更多實際應用範例

如果你需要更多範例,請參考官網 Docker Documentation - Dockerization examples 這個網頁,裡面包含像是 MongoDB、PostgreSQL 之類的應用使用 Dockerfile 來 build image 的過程。

Reference:

Docker Documentation - Dockerfile reference

Stackoverflow - What is the difference between the COPY and ADD commands in a Dockerfile?

Stackoverflow - What is the difference between CMD and ENTRYPOINT in a Dockerfile?

segmentfault - Dockerfile里指定执行命令用ENTRYPOING和用CMD有何不同?