概述
Docker依賴Dockerfile來自動構建鏡像。Dockerfile的語法雖然簡單,但是如何編寫Dockerfile來減小鏡像大小并加快鏡像構建速度卻需要實踐經驗的累積。本節介紹Dockerfile編寫的一些最佳實踐,以幫助編寫出高效的Dockerfile。
通過.dockerignore排除文件
Docker在構建鏡像時,會將Dockerfile目錄中的所有文件收集到進程中。對于不需要參與構建的文件,可以通過.dockerignore文件來進行排除,從而減小鏡像的大小。.dockerignore的語法類似.gitignore,示例如下:
.git/
node_modules/該示例排除了Dockerfile目錄中的.git和node_modules兩個文件夾。
容器只運行單個應用
Docker雖然支持運行多個進程,例如將前端、后端和數據庫都運行在一個Docker容器中,但這樣做會帶來一些問題:
構建時間長,修改某個應用導致整體重新構建
鏡像體積大
不同應用所需資源不同,擴展時導致資源浪費
因此,最好將服務拆分成不同的應用,對每個應用單獨進行鏡像構建和部署。例如一個依賴node.js和MySQL的服務的Dockerfile原本需要安裝兩者的依賴:
RUN apt-get install -y nodejs mysql進行拆分之后,node.js和MySQL服務可以單獨部署。
node.js服務的Dockerfile包含:
RUN apt-get install -y nodejs
MySQL服務的Dockerfile包含:
RUN apt-get install -y mysql
避免安裝不必要的包
避免安裝額外的或不必要的包,可以降低鏡像的復雜度,減少鏡像大小以及構建時間。當需要更新包時,推薦使用apt-get install -y xxx來升級指定的包,避免安裝不必要的依賴。apt-get upgrade會自動更新所有的依賴包,導致構建過程不確定,可能產生不一致的鏡像,因此應該盡量避免使用。
減少鏡像層并利用緩存
Docker鏡像是分層的,Dockerfile中的每個指令都會創建一個新的鏡像層,鏡像層將被緩存和復用。當Dockerfile的指令修改了,復制的文件變化了,或者構建鏡像時指定的變量不同了,對應的鏡像層緩存就會失效,且之后的鏡像層緩存都會失效。
對于以下Dockerfile:
FROM ubuntu
ADD . /app
RUN apt-get update
RUN apt-get install -y nodejs
RUN cd /app && npm install
CMD npm start可以將兩條apt-get相關的RUN指令合并,來減少鏡像層,并且防止apt-get update命中緩存從而導致apt-get install安裝過期的依賴。同時,將apt-get指令前移,防止因為每次源代碼變動生成新的鏡像層,從而導致apt-get指令緩存失效。所以最終調整的結果為:
FROM ubuntu
RUN apt-get update && apt-get install -y nodejs
ADD ./app
RUN cd /app && npm install
CMD npm start刪除指令生成的多余文件
假設我們更新了apt-get源,下載解壓并安裝了一些軟件包,它們都保存在/var/lib/apt/lists/目錄中。但是,運行應用時Docker鏡像中并不需要這些文件。所以最好將它們刪除,防止Docker鏡像變大。示例:
RUN apt-get update \
&& apt-get install -y nodejs \
&& rm -rf /var/lib/apt/lists/* # 刪掉由apt-get update生成的目錄指定基礎鏡像的標簽
當鏡像沒有指定標簽時,將默認使用latest 標簽。因此,FROM ubuntu 指令等同于FROM ubuntu:latest。當鏡像更新時,latest標簽會指向不同的鏡像,這時構建鏡像有可能失敗。因此,若非的確需要使用最新版的基礎鏡像,最好指定確定的鏡像標簽,例如FROM ubuntu:16.04。
選擇合適的基礎鏡像
對于不同的應用,應該選擇最合適的基礎鏡像。例如,如果只需要運行node程序,則可以使用node鏡像替代ubuntu鏡像,并且通過使用極小化的alpine版本能進一步降低鏡像大小。示例:
FROM node:7-alpine
ADD ./app
RUN cd /app && npm install
CMD npm start使用多階段構建
多階段構建是在Dockerfile中使用多個FROM語句來構建鏡像的方法。由于每個構建階段只包含必要的依賴項和文件,因此可以提升構建速度并減小最終鏡像的大小。以下示例使用了二階段構建,第一階段完成源代碼的編譯,第二階段在scratch空鏡像中運行第一階段得到的可執行文件,從而降低最終鏡像大小。
# 第一階段
FROM golang:1.16 as builder
WORKDIR /go/src
COPY myapp.go ./
RUN go build myapp.go -o myapp
# 第二階段
FROM scratch
WORKDIR /server
# 引用第一階段的可執行文件
COPY --from=builder /go/src/myapp ./
CMD ["./myapp"]