天天看點

docker之Dockerfile

Dockerfile 簡介

從之前 docker commit 的學習中,我們可以了解到,鏡像的定制實際上就是定制每一層所

添加的配置、檔案。如果我們可以把每一層修改、安裝、建構、操作的指令都寫入一個腳

本,用這個腳本來建構、定制鏡像,那麼之前提及的無法重複的問題、鏡像建構透明性的問題、體積的問題就都會解決。這個腳本就是 Dockerfile。

Dockerfile是由一系列指令和參數構成的腳本,這些指令應用于基礎鏡像并最終建立一個新的鏡像。它們簡化了從頭到尾的流程并極大的簡化了部署工作。Dockerfile從FROM指令開始,緊接着跟随者各種方法,指令和參數。其産出為一個新的可以用于建立容器的鏡像。

Dockerfile 文法

Dockerfile文法由兩部分構成,注釋和指令+參數,下面以定制一個 nginx 鏡像(建構好的鏡像内會有一個 /usr/share/nginx/html/index.html 檔案):

FROM nginx
RUN echo '這是一個本地建構的nginx鏡像' > /usr/share/nginx/html/index.html           
docker之Dockerfile

開始建構鏡像

在 Dockerfile 檔案的存放目錄下,執行建構動作。

以下示例,通過目錄下的 Dockerfile 建構一個 nginx:test(鏡像名稱:鏡像标簽)。

docker build -t nginx:test .           

注:最後的 . 代表本次執行的上下文路徑,下一節會介紹。

docker之Dockerfile

Dockerfile 指令

docker之Dockerfile

FROM

FROM指令可能是最重要的Dockerfile指令。改指令定義了使用哪個基礎鏡像啟動建構流程。基礎鏡像可以為任意鏡像。如果基礎鏡像沒有被發現,Docker将試圖從Docker image index來查找該鏡像。FROM指令必須是Dockerfile的首個指令。
# Usage: FROM [image name]
FROM ubuntu            

注:

FROM scratch

這是Docker 種存在的特殊鏡像 。這個鏡像是虛拟的概念并不實際存在,表示一個空白的鏡像。對于 Linux 下靜态編譯的程式,并不需要有作業系統提供運作支援,所需的一切庫都已經在可執行檔案裡了,是以 FROM scratch 會讓鏡像體積更加小巧。

RUN 執行指令

RUN指令是Dockerfile執行指令的核心部分。它接受指令作為參數并用于建立鏡像。不像CMD指令,RUN指令用于建立鏡像(在之前commit的層之上形成新的層)。其格式有兩種:

shell 格式: RUN <指令> ,就像直接在指令行中輸入的指令一樣。

RUN echo 'Hello, Docker!' > /usr/share/nginx/html/index.html           

exec 格式

RUN ["可執行檔案", "參數1", "參數2"] ,這更像是函數調用中的格式。

Dockerfile 中每一個指令都會建立一層, RUN 也不例外。每一個 RUN 的行為,就和剛才我們手工建立鏡像的過程一樣:建立立一層,在其上執行這些指令,執行結束後, commit 這一層的修改,構成新的鏡像。
FROM debian:jessie
RUN apt-get update
RUN apt-get install -y gcc libc6-dev make
RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-3.2.5.tar.gz"
RUN mkdir -p /usr/src/redis
RUN tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1
RUN make -C /usr/src/redis
RUN make -C /usr/src/redis install           

上面的這種寫法,建立了 7 層鏡像,這是完全沒有意義的。結果就是産生非常臃腫、非常多層的鏡像,不僅僅增加了建構部署的時間,也很容易出錯。Union FS 是有最大層數限制的,比如 AUFS,曾經是最大不得超過 42 層,現在是不得超過127 層

上面的 Dockerfile 正确的寫法應該是這樣:

FROM debian:jessie
RUN buildDeps='gcc libc6-dev make' \
&& apt-get update \
&& apt-get install -y $buildDeps \
&& wget -O redis.tar.gz "http://download.redis.io/releases/redis-3.2.5.tar.gz" \
&& mkdir -p /usr/src/redis \
&& tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \
&& make -C /usr/src/redis \
&& make -C /usr/src/redis install \
&& rm -rf /var/lib/apt/lists/* \
&& rm redis.tar.gz \
&& rm -r /usr/src/redis \
&& apt-get purge -y --auto-remove $buildDeps           
  • 之前所有的指令隻有一個目的,就是編譯、安裝 redis 可執行檔案。這裡僅僅使用一個 RUN 指令,并使用 && 将各個所需指令串聯起來。将其簡化為了1 層。
  • Dockerfile 支援 行尾添加 \ 的指令換行,以及行首 # 進行注釋的格式。
  • 最後清理了所有下載下傳、展開的檔案,并且還清理了 apt 緩存檔案。鏡像是多層存儲,每一層的東西并不會在下一層被删除,是以鏡像建構時,任何無關的東西都應該清理掉。

ADD

ADD指令有兩個參數,源和目标。它的基本作用是從源系統的檔案系統上複制檔案到目标容器的檔案系統。如果源是一個URL,那該URL的内容将被下載下傳并複制到容器中。

# Usage: ADD [source directory or URL] [destination directory]
ADD /my_app_folder /my_app_folder            

CMD

和RUN指令相似,CMD可以用于執行特定的指令。和RUN不同的是,這些指令不是在鏡像建構的過程中執行的,而是在用鏡像建構容器後被調用。

# Usage 1: CMD application "argument", "argument", ..
CMD "echo" "Hello docker!"           

ENTRYPOINT

配置容器啟動後執行的指令,并且不可被 docker run 提供的參數覆寫。

每個 Dockerfile 中隻能有一個 ENTRYPOINT,當指定多個時,隻有最後一個起效。

ENTRYPOINT 幫助你配置一個容器使之可執行化,如果你結合CMD指令和ENTRYPOINT指令,你可以從CMD指令中移除“application”而僅僅保留參數,參數将傳遞給ENTRYPOINT指令。

# Usage: ENTRYPOINT application "argument", "argument", ..
# Remember: arguments are optional. They can be provided by CMD
# or during the creation of a container.
ENTRYPOINT echo
# Usage example with CMD:
# Arguments set with CMD can be overridden during *run*
CMD "Hello docker!"
ENTRYPOINT echo           

ENV

ENV指令用于設定環境變量。這些變量以”key=value”的形式存在,并可以在容器内被腳本或者程式調用。這個機制給在容器中運作應用帶來了極大的便利。

# Usage: ENV key value
ENV SERVER_WORKS 4           

EXPOSE

EXPOSE用來指定端口,使容器内的應用可以通過端口和外界互動。

# Usage: EXPOSE [port]
EXPOSE 8080           

MAINTAINER

我建議這個指令放在Dockerfile的起始部分,雖然理論上它可以放置于Dockerfile的任意位置。這個指令用于聲明作者,并應該放在FROM的後面。

# Usage: MAINTAINER [name]
MAINTAINER authors_name            

USER

USER指令用于設定運作容器的UID。

# Usage: USER [UID]
USER 751           

VOLUME

VOLUME指令用于讓你的容器通路主控端上的目錄。

# Usage: VOLUME ["/dir_1", "/dir_2" ..]
VOLUME ["/my_files"]           

WORKDIR

WORKDIR指令用于設定CMD指明的指令的運作目錄。

# Usage: WORKDIR /path
WORKDIR ~/           

建構鏡像

好了,讓我們再回到之前定制的 nginx 鏡像的 Dockerfile 來。現在我們明白了這個 Dockerfile 的内容,那麼讓我們來建構這個鏡像吧。

在 Dockerfile 檔案所在目錄執行:

$ docker build -t nginx:v3 .
Sending build context to Docker daemon 2.048 kB
Step 1 : FROM nginx
 ---> e43d811ce2f4
Step 2 : RUN echo 'Hello, Docker!' > /usr/share/nginx/html/index.html
 ---> Running in 9cdc27646c7b
 ---> 44aa4490ce2c
Removing intermediate container 9cdc27646c7b
Successfully built 44aa4490ce2c           

從指令的輸出結果中,我們可以清晰的看到鏡像的建構過程。在 Step 2 中,如同我們之前所說的那樣,RUN 指令啟動了一個容器 9cdc27646c7b,執行了所要求的指令,并最後送出了這一層 44aa4490ce2c,随後删除了所用到的這個容器 9cdc27646c7b。

這裡我們使用了 docker build 指令進行鏡像建構。其格式為:

docker build [選項] <上下文路徑/URL/->           

在這裡我們指定了最終鏡像的名稱 -t nginx:v3,建構成功後,我們可以像之前運作 nginx:v2 那樣來運作這個鏡像,其結果會和 nginx:v2 一樣。

鏡像建構上下文(Context)

如果注意,會看到 docker build 指令最後有一個 .。. 表示目前目錄,而 Dockerfile 就在目前目錄,是以不少初學者以為這個路徑是在指定 Dockerfile 所在路徑,這麼了解其實是不準确的。如果對應上面的指令格式,你可能會發現,這是在指定 上下文路徑。那麼什麼是上下文呢?

首先我們要了解 docker build 的工作原理。Docker 在運作時分為 Docker 引擎(也就是服務端守護程序)和用戶端工具。Docker 的引擎提供了一組 REST API,被稱為 Docker Remote API,而如 docker 指令這樣的用戶端工具,則是通過這組 API 與 Docker 引擎互動,進而完成各種功能。是以,雖然表面上我們好像是在本機執行各種 docker 功能,但實際上,一切都是使用的遠端調用形式在服務端(Docker 引擎)完成。也因為這種 C/S 設計,讓我們操作遠端伺服器的 Docker 引擎變得輕而易舉。

當我們進行鏡像建構的時候,并非所有定制都會通過 RUN 指令完成,經常會需要将一些本地檔案複制進鏡像,比如通過 COPY 指令、ADD 指令等。而 docker build 指令建構鏡像,其實并非在本地建構,而是在服務端,也就是 Docker 引擎中建構的。那麼在這種用戶端/服務端的架構中,如何才能讓服務端獲得本地檔案呢?

這就引入了上下文的概念。當建構的時候,使用者會指定建構鏡像上下文的路徑,docker build 指令得知這個路徑後,會将路徑下的所有内容打包,然後上傳給 Docker 引擎。這樣 Docker 引擎收到這個上下文包後,展開就會獲得建構鏡像所需的一切檔案。

如果在 Dockerfile 中這麼寫:

COPY ./package.json /app/           

這并不是要複制執行 docker build 指令所在的目錄下的 package.json,也不是複制 Dockerfile 所在目錄下的 package.json,而是複制 上下文(context) 目錄下的 package.json。

是以,COPY 這類指令中的源檔案的路徑都是相對路徑。這也是初學者經常會問的為什麼 COPY ../package.json /app 或者 COPY /opt/xxxx /app 無法工作的原因,因為這些路徑已經超出了上下文的範圍,Docker 引擎無法獲得這些位置的檔案。如果真的需要那些檔案,應該将它們複制到上下文目錄中去。

現在就可以了解剛才的指令 docker build -t nginx:v3 . 中的這個 .,實際上是在指定上下文的目錄,docker build 指令會将該目錄下的内容打包交給 Docker 引擎以幫助建構鏡像。

如果觀察 docker build 輸出,我們其實已經看到了這個發送上下文的過程:

$ docker build -t nginx:v3 .
Sending build context to Docker daemon 2.048 kB
...           

了解建構上下文對于鏡像建構是很重要的,避免犯一些不應該的錯誤。比如有些初學者在發現 COPY /opt/xxxx /app 不工作後,于是幹脆将 Dockerfile 放到了硬碟根目錄去建構,結果發現 docker build 執行後,在發送一個幾十 GB 的東西,極為緩慢而且很容易建構失敗。那是因為這種做法是在讓 docker build 打包整個硬碟,這顯然是使用錯誤。

一般來說,應該會将 Dockerfile 置于一個空目錄下,或者項目根目錄下。如果該目錄下沒有所需檔案,那麼應該把所需檔案複制一份過來。如果目錄下有些東西确實不希望建構時傳給 Docker 引擎,那麼可以用 .gitignore 一樣的文法寫一個 .dockerignore,該檔案是用于剔除不需要作為上下文傳遞給 Docker 引擎的。