純乾貨!構建Dockfile鏡像的十三個最佳實踐點
小知識,大挑戰!本文正在參與“程序員必備小知識”創作活動
編寫.dockerignore文件
構建鏡像時,Docker需要先準備上下文 ,將所有需要的文件收集到進程中。默認的上下文包含Dockerfile目錄中的所有文件,但是,實際上我們並不需要.git
目錄,.vscode
目錄、.idea
目錄等內容。 .dockerignore
的作用和語法類似於 .gitignore
,可以忽略一些不需要的文件,這樣可以有效加快鏡像構建時間,同時減少Docker鏡像的大小。
樣例:
.git/
.vscode/
.idea/
一個容器只運行單個應用
從技術角度講,你可以在Docker容器中運行多個進程。你可以將數據庫,前端,後端,ssh,supervisor都運行在同一個Docker容器中。但是,這會讓你非常痛苦:
- 非常長的構建時間(如,修改前端之後,整個後端也需要重新構建)
- 非常大的鏡像大小
- 多個應用的日誌難以處理(不能直接使用stdout,否則多個應用的日誌會混合到一起)
- 橫向擴展時非常浪費資源(不同的應用需要運行的容器數並不相同)
- 殭屍進程問題(你需要選擇合適的init進程)
因此,建議大家為每個應用構建單獨的Docker鏡像。
選擇合適的基礎鏡像
合適的基礎鏡像,如scratch、busybox、alpine、distroless等鏡像。可以幫助我們減少鏡像的大小。同時,越小的鏡像表示無用的程序越少,可以大大的減少被攻擊的目標,從而提高了安全性。
將多個RUN指令合併為一個
Docker鏡像是分層的,下面這些知識點非常重要:
- Dockerfile中的每個指令都會創建一個新的鏡像層。
- 鏡像層將被緩存和複用
- 當Dockerfile的指令修改了,複製的文件變化了,或者構建鏡像時指定的變量不同了,對應的鏡像層緩存就會失效
- 某一層的鏡像緩存失效之後,它之後的鏡像層緩存都會失效
- 鏡像層是不可變的,如果我們再某一層中添加一個文件,然後在下一層中刪除它,則鏡像中依然會包含該文件(只是這個文件在Docker容器中不可見了)。
現在,我們將所有的RUN指令合併為一個。同時把apt-get upgrade
刪除,因為它會使得鏡像構建非常不確定(我們只需要依賴基礎鏡像的更新就好了)。
```Dockerfile FROM ubuntu
ADD . /app
RUN apt-get update \ && apt-get install -y nodejs \ && cd /app \ && npm install
CMD npm start ``` 記住一點,我們只能將變化頻率一樣的指令合併在一起。將node.js安裝與npm模塊安裝放在一起的話,則每次修改源代碼,都需要重新安裝node.js,這顯然不合適。
因此,正確的寫法是這樣的:
```Dockerfile FROM ubuntu
RUN apt-get update && apt-get install -y nodejs
ADD . /app
RUN cd /app && npm install
CMD npm start ```
基礎鏡像和生產鏡像的標籤不要使用latest
當鏡像沒有指定標籤時,將默認使用latest 標籤。因此, FROM ubuntu 指令等同於FROM ubuntu:latest。當時,當鏡像更新時,latest標籤會指向不同的鏡像,這時構建鏡像有可能失敗。如果你的確需要使用最新版的基礎鏡像,可以使用latest標籤,否則的話,最好指定確定的鏡像標籤。
樣例:
```Dockerfile FROM ubuntu:16.04 # 使用16.04作為標籤。
RUN apt-get update && apt-get install -y nodejs
ADD . /app
RUN cd /app && npm install
CMD npm start ```
每個RUN指令後刪除多餘文件
假設我們更新了apt-get源,下載,解壓並安裝了一些軟件包,它們都保存在/var/lib/apt/lists/
目錄中。但是,運行應用時Docker鏡像中並不需要這些文件。我們最好將它們刪除,因為它會使Docker鏡像變大。
樣例:
Dockerfile中,我們可以刪除/var/lib/apt/lists/
目錄中的文件(它們是由apt-get update
生成的)。
```Dockerfile FROM ubuntu:16.04
RUN apt-get update \ && apt-get install -y nodejs \ # added lines && rm -rf /var/lib/apt/lists/*
ADD . /app
RUN cd /app && npm install
CMD npm star ```
設置WORKDIR和CMD
WORKDIR指令可以設置默認目錄,也就是運行RUN / CMD / ENTRYPOINT
指令的地方。
CMD指令可以設置容器創建是執行的默認命令。另外,你應該講命令寫在一個數組中,數組中每個元素為命令的每個單詞。
樣例:
```Dockerfile FROM node:7-alpine
WORKDIR /app
ADD . /app
RUN npm install
CMD ["npm", "start"] ```
使用ENTRYPOINT時,用exec啟動命令(可選)
在使用entrypoint的腳本中,我們要用exec命令運行應用。不使用exec的話,我們則不能順利地關閉容器,因為SIGTERM信號會被bash腳本進程吞沒。exec命令啟動的進程可以取代腳本進程,因此所有的信號都會正常工作。
相比ADD,優先使用COPY
COPY指令非常簡單,僅用於將文件拷貝到鏡像中。ADD相對來講複雜一些,可以用於下載遠程文件以及解壓壓縮包。
樣例:
```Dockerfile FROM node:7-alpine
WORKDIR /app
COPY . /app
RUN npm install
ENTRYPOINT ["./entrypoint.sh"]
CMD ["start"] ```
設置默認的環境變量,映射端口和數據卷
運行Docker容器時很可能需要一些環境變量。在Dockerfile設置默認的環境變量是一種很好的方式。另外,我們應該在Dockerfile中設置映射端口和數據卷。
樣例:
```Dockerfile FROM node:7-alpine
ENV PROJECT_DIR=/app
WORKDIR $PROJECT_DIR
COPY package.json $PROJECT_DIR
RUN npm install
COPY . $PROJECT_DIR
ENV MEDIA_DIR=/media \ NODE_ENV=production \ APP_PORT=3000
VOLUME $MEDIA_DIR
EXPOSE $APP_PORT
ENTRYPOINT ["./entrypoint.sh"]
CMD ["start"] ``` ENV指令指定的環境變量在容器中可以使用。如果你只是需要指定構建鏡像時的變量,你可以使用ARG指令。
使用LABEL設置鏡像元數據
使用LABEL指令,可以為鏡像設置元數據,例如鏡像創建者或者鏡像説明。舊版的Dockerfile語法使用MAINTAINER指令指定鏡像創建者,但是它已經被棄用了。有時,一些外部程序需要用到鏡像的元數據,例如nvidia-docker需要用到com.nvidia.volumes.needed。
樣例:
```Dockerfile FROM node:7-alpine
LABEL maintainer "[email protected]" ```
一個鏡像可以有多個label。要指定多個labels,Docker推薦儘可能地把多個labels合併到一個LABEL指令中去。每一個LABEL指令會生成一個新的鏡像層,如果你使用多個label,將導致構建出一個低效的鏡像。
添加HEALTHCHECK
運行容器時,可以指定--restart always
選項。這樣的話,容器崩潰時,Docker守護進程(docker daemon)會重啟容器。對於需要長時間運行的容器,這個選項非常有用。但是,如果容器的確在運行,但是不可(陷入死循環,配置錯誤)用怎麼辦?
使用HEALTHCHECK指令可以讓Docker週期性的檢查容器的健康狀況。我們只需要指定一個命令,如果一切正常的話返回0,否則返回1。
樣例:
```Dockerfile FROM node:7-alpine
LABEL maintainer "[email protected]"
ENV PROJECT_DIR=/app
WORKDIR $PROJECT_DIR
COPY package.json $PROJECT_DIR
RUN npm install
COPY . $PROJECT_DIR
ENV MEDIA_DIR=/media \ NODE_ENV=production \ APP_PORT=3000
VOLUME $MEDIA_DIR
EXPOSE $APP_PORT
HEALTHCHECK CMD curl --fail http://localhost:$APP_PORT || exit 1
ENTRYPOINT ["./entrypoint.sh"]
CMD ["start"] ```
當請求失敗時,curl --fail
命令返回非0狀態。
合理調整COPY和RUN的順序
我們應該把變化最少的部分放在Dockerfile的前面,這樣可以充分利用鏡像緩存。
樣例:
源代碼會經常變化,則每次構建鏡像時都需要重新安裝NPM模塊,這顯然不是我們希望看到的。因此我們可以先拷貝package.json,然後安裝NPM模塊,最後才拷貝其餘的源代碼。這樣的話,即使源代碼變化,也不需要重新安裝NPM模塊。
```Dockerfile FROM node:7-alpine
WORKDIR /app
COPY package.json /app
RUN npm install
COPY . /app
ENTRYPOINT ["./entrypoint.sh"]
CMD ["start"] ```
參考文檔
- 2022年終總結:路雖遠,行則將至
- 監控生產環境中的機器學習模型
- 監控模型在生產環境的性能(Arize)
- DVC 使用案例(二):共享數據與模型文件
- 純乾貨!構建Dockfile鏡像的十三個最佳實踐點
- 無代碼 AI 概覽(Levity)
- 無代碼人工智能平台:成功的基石(Noogata)
- 我們為什麼創建無代碼 AI 平台 Noogata
- 無代碼人工智能:它是什麼,它為什麼重要?(Noogata)
- 快速入門DVC(三):數據與模型版本管理
- 以數據為中心的人工智能應該如何實施(Valohai)
- 如何將 MLOps 用於物聯網和邊緣設備(Valohai)
- 無代碼 AI 和 MLOps:無代碼 AI 僅用於不會代碼的終端用户(Valohai)
- 用於 MLOps 的最佳訓練編排工具(Aporia)
- 如何理解機器學習中的偏見和公平(Aporia)
- 機器學習模型監控(Aporia)
- Arize AI 對頂級 ML 團隊調查得出的 3 個結論
- MLflow 快速入門
- Algorithmia 首席執行官 Diego Oppenheimer 談從研發到人工智能的投資回報率
- 淺析Linux中的五種IO模型