前兩天跟著文件中 Get Started
的內容大致瞭解了使用 Docker 作為應用程式服務的開發流程,從單一容器的應用程式開始,到部署於單一節點的 swarm 服務,最後是多個節點、多個服務的堆疊。這幾天研究並操作了不少 Docker 相關工具,但 Docker 本身的架構又是如何呢?Docker 引擎是一個 client-server 架構的應用程式,它由三個部分組成:
這三層的關係,請參考下圖。
(圖片出自 https://docs.docker.com/engine/docker-overview/#docker-engine)
這幾天介紹的指令,都是最外層的 client,它透過中間層的 Docker API 和 server 溝通。將來開發上有需要,可以直接使用 API 及 SDK,API 文件請參考 https://docs.docker.com/develop/sdk/#choose-the-sdk-or-api-version-to-use。
我覺得 Docker 初學的重點應該是關於映像檔和容器的操作,所以今天先來看看官方文件中關於映像檔的說明。
Docker 的映像檔由多個唯讀層 (read-only layer) 所組成,每一層代表了 Dockerfile
中的一個指令,映像檔由這些唯讀層堆疊而成,每一層都是和前一層變化的差異 (delta of changes from the previous layer)。當映像檔被執行時會產生容器,並在唯讀層之上加上一可寫層 (writable layer) 或稱容器層 (container layer),所有對執行中的容器作出的改變,像是寫入新檔案,修改已存在的檔案,刪除檔案,都是寫入這一可寫層。
Dockerfile 包含了使用者在命令列中可以呼叫的指令,用來組成 Docker 所使用的映像檔。透過 docker build
指令可依序讀取 Dockerfile 中的指令以自動建立映像檔。除了 Dockerfile
之外,docker build
在建立映像檔時還需要指定一 build context,context 是指在指定目錄或 Git 儲存庫中的一組檔案。docker build
是由 Docker daemon 所運行的,而不是 CLI。build 過程第一件事是將整個 context 內容送到 daemon。這裡不是很懂文件的意思,我直覺上認為將 context 送到 daemon,可能是指將 context 中的檔案複製到容器中,但 Dockerfile
中又有 COPY
和 ADD
的指令可以指定要加到容器中的檔案。預設 Dockerfile
會位於 build context 中,也可以指定其路徑。如果要排除 context 中和 build 無關的檔案,可以使用 .dockerignore
,語法和 .gitignore
類似。
如同先前的例子中所看到的,Dockerfile
的基本格式是指令加上參數,註解則以 #
開頭,慣例上會使用大寫字母來編寫指令。例如:
# Comment
INSTRUCTION arguments
接下來介紹 Dockerfile
中可用的指令,文件中這裡分為建議 (recommendation) 及參考 (reference) 兩個部分,詳細的說明請參考 https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#dockerfile-instructions 及 https://docs.docker.com/engine/reference/builder/,以下僅列出簡要的用途和格式,並舉例說明。
Dockerfile
原則上會從 FROM
指令開始,用來指定這個映像檔所使用的基礎映像檔 (base image) 以讓後續的指令使用,基本格式是
* FROM <image> [AS <name>]
* FROM <image>[:<tag>] [AS <name>]
* FROM <image>[@<digest>] [AS <name>]
FROM ubuntu:14.04
每一個 RUN
指令會在現有映像檔之上加入新的一層,指令於該層被執行並提交結果。有兩種格式,第一種是 shell 形式,會透過 shell 來執行,第二種是 exec 形式,請參考以下格式。
* RUN <command> (shell form)
* RUN ["executable", "param1", "param2"] (exec form)
RUN /bin/bash -c 'source $HOME/.bashrc; echo $HOME'
RUN ["/bin/bash", "-c", "echo hello"]
一個 Dockerfile
中只能有一個 CMD
指令,若出現多次則最後一次會產生作用。它的作用是為執行中的容器提供預設行為 (provide defaults)。和 CMD
不同之處在於 RUN
是在建立 (build) 映像檔的過程中會執行的指令,CMD
則是容器運行時所執行的指令,格式如下:
* CMD ["executable","param1","param2"] (exec form, this is the preferred form)
* CMD ["param1","param2"] (as default parameters to ENTRYPOINT)
* CMD command param1 param2 (shell form)
CMD echo "This is a test." | wc -
CMD ["/usr/bin/wc","--help"]
為映像檔添加元資料 (metadata),格式如下:
* LABEL <key>=<value> <key>=<value> <key>=<value> ...
LABEL "com.example.vendor"="ACME Incorporated"
LABEL com.example.label-with-value="foo"
用來指定此容器在執行時要揭露的 port。若要從容器外部連接此 port,需要在 docker run
執行此容器時使用 -p
旗標,讓它對應本機的 port。格式如下:
* EXPOSE <port> [<port>/<protocol>...]
EXPOSE 80/tcp
EXPOSE 80/udp
用來設置環境變數,可於 build 階段於後續的指令中使用,或者是在容器執行時作為環境變數,格式如下:
* ENV <key> <value>
* ENV <key>=<value> ...
ENV PG_MAJOR 9.3
ENV PG_VERSION="9.3.4"
複製指定的檔案、目錄或遠端檔案 URL,將其加入映像檔檔案系統中的指定位置。來源為 build context 的相對路徑,且必須為位於 build context 之中 ,目的則為絕對路徑或 WORKDIR 的相對路徑。若來源檔案為本機的 tar 壓縮檔,會解開成目錄複製到目的位置。格式範例如下:
* * ADD [--chown=<user>:<group>] <src>... <dest>
* ADD [--chown=<user>:<group>] ["<src>",... "<dest>"] (this form is required for paths containing whitespace)
ADD hom* /mydir/ # adds all files starting with "hom"
ADD test relativeDir/ # adds "test" to `WORKDIR`/relativeDir/
ADD test /absoluteDir/ # adds "test" to /absoluteDir/
ADD --chown=55:mygroup files* /somedir/
用途和 ADD
類似,但只是單純的複製,沒有解開本地 tar 檔複製或支援遠端 URL 的功能。如果是一般的複製,文件建議使用 COPY
指令。
* COPY [--chown=<user>:<group>] <src>... <dest>
* COPY [--chown=<user>:<group>] ["<src>",... "<dest>"] (this form is required for paths containing whitespace)
ENTRYPOINT
的用途和 CMD
類似,都是用來在容器中執行指令。
* ENTRYPOINT ["executable", "param1", "param2"] (exec form, preferred)
* ENTRYPOINT command param1 param2 (shell form)
文件中說 ENTRYPOINT
的使用時機是希望將容器當作一個執行檔來使用,就可以使用 ENTRYPOINT
來指定這個執行檔應該執行的命令,在這種情況下,CMD
可用來提供預設的旗標,例如:
ENTRYPOINT ["s3cmd"]
CMD ["--help"]
執行 docker run sc3md
,就會顯示指令的幫助說明,或者以 docker run s3cmd ls s3://mybucket
來執行指令。關於 ENTRYPOINT
和 CMD
的差異及搭配使用方式,文件有進一步的說明如下:
Dockerfile
中至少要有一個 CMD
或 ENTRYPOINT
指令。ENTRYPOINT
指令。CMD
應以作為 ENTRYPOINT
的預設參數,或在容器中執行任意 (ad-hoc) 指令的方式使用。CMD
值會被覆寫。用來建立一個掛載點,以掛載本機或其他容器的卷宗。要被掛載的卷宗或檔案路徑要在執行容器時指定。範例如下,可用JSON 陣列指定多個掛載點。
VOLUME ["/data"]
用來指定後續 RUN
、CMD
及 ENTRYPOINT
指令或容器中的使用者名稱、群組名稱。格式及範例如下:
* USER <user>[:<group>]
* USER <UID>[:<GID>]
FROM microsoft/windowsservercore
# Create Windows user in the container
RUN net user /add patrick
# Set it for subsequent commands
USER patrick
用來設定在 RUN
、CMD
、ENTRYPOINT
、COPY
、ADD
指令中的工作目錄。若工作目錄不存在會被建立,格式如下:
* WORKDIR /path/to/workdir
用來設定在 build 時期會用到的變數,可在 docker build
指令使用 --build-arg <name>=<value>
的形式傳入,也可在 Dockerfile
中指定變數預設值。格式如下:
* ARG <name>[=<default value>]
FROM busybox
ARG user1
ARG buildno=2
用來指定當此映像檔要被用來建立其他映像檔時,必須先執行的指令。它會加在某個指示的開頭(ONBUILD
除外),格式範例如下:
* ONBUILD [INSTRUCTION]
ONBUILD ADD . /app/src
ONBUILD RUN /usr/local/bin/python-build --dir /app/src
用來指定後續指令預設使用的 shell 類型,格式範列如下:
* SHELL ["executable", "parameters"]
# Executed as powershell -command Write-Host hello
SHELL ["powershell", "-command"]
RUN Write-Host hello
除了介紹編寫 Dockerfile
中會用到的指令外,文件也說明了編寫 Dockerfile
的原則和最佳實踐 (best practice)。一般來說會建立一個空的目錄作為 build context,在其中編寫 Dockerfile
,只把有需要的檔案複製至該目錄,不要在映像檔中安裝不必要的套件。其他的最佳實踐方式請參考文件說明。