1. 程式人生 > Docker入門教學 >Dockerfile多階段構建映象

Dockerfile多階段構建映象

之前學習部署 Docker 應用時,我們搭建過一個 redis 服務,然後編寫並運行了一個統計訪問次數的 flask 應用。

現在,我們使用 Dockerfile,將這個 flask 應用也製作成映象,此外,在這個映象中,可以包含一個 helloworld 二進位制程式,這個 helloworld 的原始碼就是我們學習 rootfs 時用到的 helloworld.c。

1. 實戰:直接構建映象

首先 我們需要新建一個目錄 dockerfiledir,用於存放 Dockerfile 檔案。

mkdir dockerfiledir
# 在這個目錄下新建個空檔案 Dockerfile,之後填充內容 
touch dockerfiledir/Dockerfile

新建一個目錄code,用來存放flask和c的原始碼。

mkdir code

將之前 app.py 和 helloworld.c 兩個原始碼檔案放入到 code 目錄下,當前的目錄結構應該是這樣的:

圖片描述

進入 dockerfiledir 目錄,編輯 Dockerfile 檔案:

# 從 ubuntu系統映象開始構建
FROM ubuntu	
# 標記映象維護者資訊
MAINTAINER user <[email protected]>
# 切換到映象的/app目錄,不存在則新建此目錄
WORKDIR /app
# 將 宿主機的檔案拷貝到容器中
COPY ../code/app.py . COPY ../code/helloworld.c . # 安裝依賴 編譯helloworld RUN apt update >/dev/null 2>&1 && \ apt install -y gcc python3-flask python3-redis >/dev/null 2>&1 && \ cc /app/helloworld.c -o /usr/bin/helloworld # 設定執行使用者為user RUN useradd user USER user # 設定flask所需的環境變數
ENV FLASK_APP app # 預設啟動執行的命令 CMD ["flask", "run", "-h", "0.0.0.0"] # 將flask的預設埠暴露出來 EXPOSE 5000

然後執行:

docker build .

出現如下報錯:

COPY failed: Forbidden path outside the build context: ../code/app.py ()

解決這個問題,需要引入一個重要的概念——構建上下文。

docker build .命令在執行時,當前目錄.被指定成了構建上下文,此目錄中的所有檔案或目錄都將被髮送到 Docker 引擎中去,Dockerfile中的切換目錄和複製檔案等操作只會對上下文中的內容生效。

Tips:在預設情況下,如果不額外指定 Dockerfile 的話,會將構建上下文對應的目錄下 Dockerfile 的檔案作為 Dockerfile。但這只是預設行為,實際上 Dockerfile 的檔名並不要求必須為 Dockerfile,而且並不要求必須位於上下文目錄中,比如可以用 -f ../demo.txt引數指定父級目錄的demo.txt檔案作為 Dockerfile。

一般來說,我們習慣使用預設的檔名 Dockerfile,將其置於映象構建上下文目錄.中。

我們需要將 code 目錄納入到上下文中,一個直接的方法是,調整dockerfile中的COPY指令的路徑。

# 將 .. 改為 .
COPY ./code/app.py .
COPY ./code/helloworld.c .

然後將 code 所在的目錄指定為構建上下文。由於我們當前的目錄是 dockerfiledir,所以我們執行:

docker build -f ./Dockerfile ..

如果你留意檢視構建過程,會發現類似這樣的提示:

Sending build context to Docker daemon 421.309 MB

如果..目錄除了code和dockerfiledir,還包含其他的檔案或目錄,docker build也會將這個資料傳輸給Docker,這會增加構建時間。
避免這種情況,有兩種解決方法:

  • 使用.dockerignore檔案:在構建上下文的目錄下新建一個.dockerignore檔案來指定在傳遞給 docker 時需要忽略掉的檔案或資料夾。.dockerignore 檔案的排除模式語法和 Git 的 .gitignore 檔案相似。

  • 使用一個乾淨的目錄作為構建上下文(推薦):使用 Dockerfile 構建映象時最好是將 Dockerfile 放置在一個新建的空目錄下。然後將構建映象所需要的檔案新增到該目錄中。

在我們當前的示例中,將code目錄移入dockerfiledir。

mv ../code .

現在的目錄層級如下:

執行 docker build -t myhello . 執行構建即可獲得我們的自定義映象 myhello。

使用映象 myhello 建立 myhello 容器:

# 這裡使用--net=host,方便使用之前章節中部署的redis容器服務,與之進行資料交換
docker run -dit --net=host  --nauser myhello myhello 

確保部署之前的 redis 容器正常啟動,然後在 Docker 宿主機的瀏覽器中訪問http://127.0.0.1:5000
圖片描述

說明 myhello 中的 flask 應用已經正常運行了。接下來,我們再執行測試一下編譯的 helloworld。

docker exec myhello /usr/bin/helloworld

得到輸出:

Hello, World!

Tips: myhello容器已經完成任務,記得執行docker rm -f myhello刪除它.

2. 改進: 使用多階段構建

在映象構建過程中,我們的 helloworld.c 原始碼以及相關編譯工具和依賴也被構建到了映象中,這導致我們最終得到的映象偏大。

理想狀態應該是使用了一個系統映象生成的容器,編譯原始碼後再將編譯的程式匯入到最終的映象中,這樣就會縮減體積,並且將不同目的的操作有效分離開,但是按照我們之前掌握的知識,這樣實現需要兩個Dockerfile 檔案。

使用多階段構建,我們可以在一個 Dockerfile 中使用多個 FROM 語句。每個 FROM 指令都可以使用不同的映象,並表示開始一個新的構建階段。很方便的將一個階段的檔案複製到另外一個階段,在最終的映象中保留下需要的內容即可。

我們還是在 Dockerfile 檔案的同一目錄,新建一個新的構建指令碼,命名為 Dockerfile-multi-stage 便於區分:

#從ubuntu映象開始構建, 將第一階段命名為`build`,在其他階段需要引用的時候使用`--from=build`引數即可。
FROM ubuntu AS build
# 將宿主機的原始碼拷貝到映象中
COPY ./code/helloworld.c .
# 安裝依賴 並編譯原始碼
RUN apt update >/dev/null 2>&1 && \
    apt install -y gcc >/dev/null 2>&1 && \
    cc helloworld.c -o /usr/bin/helloworld

# 第二階段 從官方的python基礎映象開始構建
FROM python
# 映象維護者資訊
MAINTAINER user <[email protected]>
# 將第一階段構建的helloworld 匯入到此映象中
COPY --from=build /usr/bin/helloworld /usr/bin/helloworld
# 安裝flask 和 redis 的依賴
RUN pip install flask redis >/dev/null 2>&1 
# 設定映象在切換到/app目錄路徑
WORKDIR /app
# 將原始碼匯入到映象
COPY ./code/app.py .
# 設定執行使用者為user
RUN useradd user
USER user
# 設定flask所需的環境變數
ENV FLASK_APP app
# 預設啟動執行的命令
CMD ["flask", "run", "-h", "0.0.0.0"]
# 將flask的預設埠暴露出來
EXPOSE 5000

執行 build 命令:

docker build -f Dockerfile-multi-stage -t myhello-multi-stage .

使用此映象執行一個容器:

# 這裡使用--net=host,方便使用之前章節中部署的redis容器服務,與之進行資料交換
docker run -dit --net=host --name myhello-multi-stage myhello-multi-stage

自行測試一下這個容器吧。

3. 小結

通過以上內容,相信大家對 Dockerfile 的使用又有了新的認知,我們在構建映象的時候,一定要有合理的規劃, 在自己不熟悉的基礎映象上定義映象的時候,不妨先用它執行一個容器,在容器中過一遍流程, 弄清最終的映象中到底應該包含哪些內容,再來調整構建指令碼。

這裡有一些 Dockerfile 的一般規範:

  1. 通過 Dockerfile 構建的映象所啟動的容器越快越好,這樣可以快速啟停增刪容器服務(下面幾條也是為第1條服務的);
  2. 避免安裝不必要的包,必要時使用多階段構建;
  3. 一個容器儘量只專注做一件事情;
  4. 最小化映象層數, 將重複功能的 RUN、COPY、ADD 等指令縮減合併, 但一定要保證 Dockerfile 可讀性。

當然,這些建議僅供參考,不要拘泥於它,要根據自己的使用場景來做權衡。