Docker – Dockerfile 指令教學,含範例解說


在前面介紹Docker的文章中,我們都是從Docer Hub中下載映像檔(Image)來建立Container,這些映像檔可能是軟體開發商所釋出,或者是第三方人士加值過的版本,這種做法的好處是很方便,但缺點則是映像檔包含的功能、工具或版本無法滿足自己的需求,此時利用 Dockerfile 客製化一個符合需求的映像檔,就是一個很好的解決方案。

Docker - Dockerfile 指令教學,含範例解說

在閱讀正文之前,建議要對Docker有所了解,新手可以先閱讀之前的二篇基本介紹,建立一點基本概念:
Docker – 新手入門,快速安裝與基本指令介紹
Docker Container 指令:Docker run & Docker exec

什麼是Dockerfile ?

  • 是一個文字檔,由一行一行的指令所組成,用來描述這個映像檔應該長成怎麼樣
  • 利用Dockerfile可以建構/客製化出自己獨一無二的映像檔
  • 由於Dockerfile中可以清楚的知道映像檔的組成,因此,在安全性上會有所提升
  • 因為是純文字檔,所以檔案很小、很容易分享

Dockerfile 的組成

基本上Dockerfile是由一行一行的指令列所組成,一行指令對Image來說就是一層的資料層(Layer),一個Image就是靠這樣一層一層的資料累加上去,最後才編譯出自己想要的映像檔,就像蓋房子一樣。

常見Dockerfile內容中的指令如下:

# 開頭代表註解

文件中可使用#符號來進行註解。

FROM

基底映像檔,必需是「第一個」指令行,指定這個映像檔要以哪一個Image為基底來建構,格式為FROM <image>FROM <image>:<tag>,範例如下:

FROM ubuntu:15.04 或
FROM ubuntu

MAINTAINER

映像檔維護者,把它想成是作者即可,格式為MAINTAINER <name>,範例如下:

MAINTAINER John 或
MAINTAINER john@myemail.com 或
MAINTAINER John john@myemail.com

LABEL

設定映像檔的Metadata資訊,例如:作者、EMail、映像檔的說明等,格式為:LABEL <key>=<value> <key>=<value> <key>=<value> …

簡單的來說就是以Key、Value來組成,它可以一行一組,也可以把全部合在一行撰寫,若多組合在一行,則每一組設定中間用空白鍵隔開即可,範例如下:

LABEL description="這是LABEL的範例" version="1.0" owner="靖技場"

和MAINTAINER相比,建議使用LABEL來設定會比較方便,另外,如果要查詢LABEL的資訊,則可以下docker inspect來查詢

RUN

執行指定的指令,每加一個RUN,就會在基底映像層加上一層資料層,以此類推,一層一層的建構起我們最後想要的映像檔,例如我們可以利用RUN來安裝套件,其格式分為二種:

  1. RUN <command>:以shell的形式執行,Linux的預設是/bin/sh -c,而Windows上的預設環境則是cmd /S /C
  2. RUN ["executable", "param1", "param2"]:以exec的形式執行指令,例如Linux上不想用預設的shell執行指令,那麼就可以透過RUN [“/bin/bash”, “-c”, “echo hello”]指定想要的shell

shell形式與exec形式有什麼不同呢?官方有提到

Unlike the shell form, the exec form does not invoke a command shell. This means that normal shell processing does not happen. For example, RUN [ “echo”, “$HOME” ] will not do variable substitution on $HOME

意思是說exec執行的方式不會使用command shell,所以執行 RUN [ “echo”, “$HOME” ] 這樣的指令列, $HOME 這個變數是不會被替代(填入值)的,也就是直接輸出「$HOME」,但如果你想要有Shell處理的功能,則可以自行指定shell來達成:RUN [ “sh”, “-c”, “echo $HOME” ]

在使用RUN指令時,有以下注意要點:

  • 如果想要執行的指令很長,可以利用\符號來換行,比較容易閱讀
  • 使用exec形式執行時,必需使用JSON array的格式,因此,請使用雙引號
  • 每一個RUN就會新增一層資料層,為了減少不必要的資料層,可以利用&&來串連多個命令

簡單的RUN指令範例如下:

# install packages, only for demo
RUN mkdir -p /home/demo/docker
RUN ["apt-get", "install", "python3"]
RUN apt-get update && apt-get install -y --force-yes apache2 \
    firefox \
    php5

CMD

設定映像檔啟動為Container時預設要執行的指令,其指令共支援三種格式:

  1. CMD [“executable”,”param1″,”param2″]:exec形式,官方推薦此種方式
  2. CMD [“param1″,”param2”]:適用於有定義ENTRYPOINT指令的時候,CMD中的參數會做為ENTRYPOINT的預設參數
  3. CMD command param1 param2:會以shell的形式執行,預設是在「/bin/sh -c」下執行,適合在需要互動的指令時

使用CMD的注意事項:

  • Dockerfile中只能有一行CMD,若有多行CMD,則只有最後一行會生效
  • 若在建立Container時有帶執行的命令,則CMD的指令會被蓋掉,例如:執行docker run <image id>時,CMD所定義的指令會被執行,但當執行docker run <image id> bash時,Container就會執行bash,而原本CMD中定義的值就會覆蓋

CMD範例如下:

CMD echo "This is a test." | wc -
CMD ["/usr/bin/wc","--help"]
CMD [ "sh", "-c", "echo $HOME" ]
#作為ENTRYPOINT的參數使用
CMD ["Hello"]

ENTRYPOINT

和CMD一樣,用來設定映像檔啟動Container時要執行的指令,但不同的是,ENTRYPOINT一定會被執行,而不會有像CMD覆蓋的情況發生,支援二種格式:

  1. ENTRYPOINT [“executable”, “param1”, “param2”]:exec形式,官方推薦此種方式
  2. ENTRYPOINT command param1 param2:shell的形式

使用ENTRYPOINT的注意事項:

  • Dockerfile中只能有一行ENTRYPOINT,若有多行ENTRYPOINT,則只有最後一行會生效
  • 若在建立Container時有帶執行的命令,ENTRYPOINT的指令不會被覆蓋,也就是一定會執行
  • 如果想要覆蓋ENTRYPOINT的預設值,則在啟動Container時,可以加上「–entrypoint」的參數,例如:docker run –entrypoint

舉個實例,來看看ENTRYPOINT與CMD的關係,假設Dockerfile中的定義如下:

ENTRYPOINT ["/bin/echo", "Hello"]
CMD ["World"]

如果是使用docker run -it <image>來啟動Container,那麼輸出的結果為「Hello World」,但如果是用docker run -it <image> Docker來啟動,則輸出結果會變成「Hello Docker」,因為CMD的值被覆蓋掉了

COPY

複製本地端的檔案/目錄到映像檔的指定位置中,其格式為:

  • COPY [–chown=<user>:<group>] <src>… <dest>
  • COPY [–chown=<user>:<group>] [“<src>”,… “<dest>”]

使用COPY的注意事項:

  • Dockerfile中只能有一行ENTRYPOINT,若有多行ENTRYPOINT,則只有最後一行會生效
  • 指令的來源位置可以多個
  • 如果目的位置是目錄的話,記得最後要以/結尾,例如:/mypath/
  • 目的位置可以是絕對路徑或者相對於WORKDIR定義值的相對路徑
  • 若目的位置不存在,會自動建立

COPY的範例如下:

COPY file1.txt file2.js file3.json ./
COPY ["file1.txt", "file2.js", "file3.json" "./"]

ADD

和COPY一樣,可將本地端的檔案/目錄加到映像檔的指定位置內,其格式為:

  • ADD [–chown=<user>:<group>] <src>… <dest>
  • ADD [–chown=<user>:<group>] [“<src>”,… “<dest>”]

雖然ADD和CMD功能類似,但有二點最大的不同:

  1. ADD的來源路徑支援URL,也就是說可以加入遠端的檔案,COPY則不支援URL
  2. 若來源檔案是壓縮檔(副檔名為gzip、bzip2、xz),則使用ADD加入檔案時會自動解壓縮,而COPY不會

ADD的範例如下:

ADD file1.txt file2.js file3.json ./
# ENV_DEMO_VALUE 是用ENV指令所設定的環境變數
ADD https://www.google.com/demo.gzip $ENV_DEMO_VALUE

除非你有自動解壓的需求,不然一般建議會使用「COPY」來加入檔案

EXPOSE

宣告在映像檔中預設要使用(對外)的連接埠,格式如下:

  • EXPOSE <port> [<port>/<protocol>…]

EXPOSE預設的協定是TCP,但如果不是要TCP的話,可以自行指定,範例如下:

EXPOSE 80/tcp
EXPOSE 80/udp

使用EXPOSE所定義的連接埠並不會自動的啟用,而只是做提示的作用而已,要將連接埠啟用需要在執行docker run時,搭配-p-P的參數來啟用。

小寫的-p可以自行指定與主機關聯的連接埠,例如:

docker run -p 80:80/tcp -p 80:80/udp demo

大寫的-P則會啟用所有EXPOSE所定義的連接埠,並動態(隨機)的關聯到主機的連接埠,例如:EXPOSE 80 可能隨機關聯到主機的 45123 連接埠,其範例如下:

docker run -P demo

ENV

設定環境變數,支援二種格式:

  1. ENV <key> <value>:Key後面的第一個空白鍵後會視為Value
  2. ENV <key>=<value> …:用等於符號來定義,每一組中間以空白鍵隔開,我個人比較喜歡這種形式,不容易搞混

ENV範例如下:

ENV demoPATH="/var/log" demoVer="1.0"
# 設置「/tmp/test.txt」給demoFile變數
ENV demoFile /tmp/test.txt

# 使用環境變數的例子,有沒有用大括號都可以
COPY debug.log ${demoPATH}
ADD $demoFile /foo

使用ENV設置環境變數後,在Dockerfile中其他的指令就可以利用,之後在建起來的Container裡也可以使用該變數

VOLUME

建立本機或來自其他容器的掛載點,指令格式如下:

  • VOLUME [“/data”]

VOLUME的值可以是JSON的Array格式,也可以是純文字,例如下以範例:

VOLUME ["/var/log/"] 或
VOLUME ["/demo1","/demo2"] 或
VOLUME /var/log 或
VOLUME /var/log /var/db

要特別注意的是使用VOLUME來定義掛載點時,是無法指令本機對應的目錄的,對應到哪個目錄是自動產生,我們可以透過docker inspect來查詢目錄資訊

WORKDIR

設定工作目錄,其格式如下:

  • WORKDIR /path/to/workdir

當設定WORKDIR後,Dockerfile中的RUN、CMD、ENTRYPOINT、COPY、ADD等指令就會在該工作目錄下執行,以下是官方的示範:

WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd

在上面的範例中,pwd最後會在/a/b/c目錄下執行,如果目錄不存在,系統會幫忙自動建立

USER

指定運行Container時的用戶名稱或UID,其格式如下:

  • USER <user>[:<group>]
  • USER <UID>[:<gid>]

在定義了USER後,則Dockerfile中的RUN、CMD、ENTRYPOINT等指令便會以USER指定的用戶來執行,前提條件是該用戶必需是已存在的,否則指定會失敗,範例如下:

RUN groupadd -r tester && useradd -r -g tester tester
# 指定用戶名稱
USER tester

# 或使用UID來指定
USER 1000

ARG

設定在建置映像檔時可傳入的參數,即定義變數名稱以及變數的預設值,其格式為:

  • ARG <name>[=<default value>]

ARG和ENV的功能類似,都可以設定變數,但是ARG設定的值是供建置映像檔時使用(搭配docker build指令),在Container中是無法使用這些變數的,相反地,ENV的值則可以在Container中存取,例如ARG的定義如下:

ARG Param1
ARG Param2=somevalue

建構映像檔案,可利用–build-arg <varname>=<value>來指定參數,例如:

docker build --build-arg Param1=demo -t myimage:v1 .

在上面的例子中,我們在docker build中利用–build-arg <varname>=<value>參數將Param1的值變更為「demo」,而Param2的值並沒有指定,所以保留預設值「somevalue」

ONBUILD

若這個映像檔是作為其他映像檔的基底時,便需要定義ONBUILD指令,格式為:

  • ONBUILD [INSTRUCTION]

ONBUILD後面接的指令在自建的映像檔中不會被執行,只有當這個映像檔是作為其他映像檔的基底時才會被觸發,例如A映像檔的Dockerfile定義如下(假設名稱為A-Image):

...(以上略)
ONBUILD ADD . /home/tmp
ONBUILD mkdir -p /home/demo/docker
...(以下略)

此時如果B映像檔是以A映像檔為基底,則A映像檔中的ONBUILD指令就會被觸發,等於是以下指令:

# 以A映像檔為基底
FROM A-Image

# 觸發A映像檔ONBUILD的指令,即會自動執行下面二個指令行
# 下面二行指令不用自己加,Docker會自動去執行,這邊寫出只是方便做說明
ADD . /home/tmp
mkdir -p /home/demo/docker

如何使用Dockerfile ?

介紹了那麼多的指令,最終的目的就是利用Dockerfile來建立我們自己的映像檔,其指令為docker build,範例如下:

# 在目前目錄尋找Dockerfile或dockerfile
docker build -t myimage:v1 .

-t:Name and optionally a tag in the ‘name:tag’ format,指定映像檔名稱、標籤

在上面的範例中,是假設Dockerfile在當前目錄下,因此會以.結尾,若是在不同目錄,則可以直接接Dockerfile所在目錄或用-f來指定Dockerfile位置,例如:

# 後面接Dockerfile的所在目錄
docker build -t myimage:v2 ./docker
docker build -f /path/to/a/Dockerfile -t myimage:v3 .

-f來指定Dockerfile的位置時,後面接的目錄(及其子目錄)需要能夠找到Dockerfile,否則會出現context錯誤

小結:Dockerfile裡面的指令也不少,但因篇幅的關係這邊沒有列出所有的指令,但應該也足夠滿足大部分的需求,想要更深入研究的話,建議可以參考官方的文件,但我個人認為最快的學習方法可以自己動手做做看,比較能夠體會每個指令到底有什麼功用

延伸閱讀:
Best practices for writing Dockerfiles
Dockerfile Reference