最近在架這個網站的時候,在我的Home Server建了一個CI/CD Pipeline,為了可以在CI build Docker Image時節省Image的大小,學了一下Multi-Stage build,這篇文章就來記錄一下。
Image Layer
首先最重要的概念就是,Docker的每個Image都是由多個Layer組成的,當我們在Dockerfile裡每執行一個指令(e.g.FROM, RUN, COPY...)時,都會在原有的Layer之上再建立一個新的Layer,且該指令對檔案的任何操作或是Diff都只會記錄在這個Layer中(如果是改上一個Layer就有的檔案會複製一份再改,不影響上個Layer的完整性,Copy-on-Write)。
所以說當我們要build一個新的Image時,裡面可能就會包含很多不同的階段,像是安裝套件、編譯、打包可執行檔等等,每個步驟可能都會在對應的Layer裡產生很多最終執行檔不需要的東西,例如快取、編譯工具,導致整包Image很肥大。
FROM node:18
WORKDIR /app
# 複製 package.json 並安裝所有依賴 (包含 devDependencies)
COPY package*.json ./
RUN npm install
# 複製所有原始碼並進行編譯
COPY . .
RUN npm run build
# 啟動伺服器 (通常是 npm start)
CMD ["npm", "start"]
# 【結果】映像檔大小:可能高達 1GB+ (包含 node_modules 和原始碼)
Multi-Stage Build
為了處理這個問題 Docker提供了多階段build的功能,我們只需要使用FROM AS就可以把每個不同階段拆開來,只傳遞該階段重要的結果(Artifact),其他用不到的東西都可以直接丟掉。
# --- 第一階段:編譯階段 (命名為 build-stage) ---
FROM node:18 AS build-stage
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
# 執行 build 指令,產生 dist 資料夾
RUN npm run build
# --- 第二階段:部署階段 (使用 Nginx) ---
FROM nginx:stable-alpine
# 【關鍵】從 build-stage 階段只把編譯好的靜態檔案複製到 Nginx 的目錄
COPY --from=build-stage /app/dist /usr/share/nginx/html
# Nginx 預設會啟動,所以不需要額外的 CMD
# 【結果】映像檔大小:極小 (僅包含 Nginx 及其靜態資源,約幾 MB)
小細節
有時我們的指令會安裝很多東西或產生快取,這時怎麼清理會是個重點 錯誤寫法:
# 第一層:安裝 git 並產生了大量的暫存檔
RUN apt-get update && apt-get install -y git
# 第二層:試圖刪除暫存檔
RUN rm -rf /var/lib/apt/lists/*
由於每個指令都是單獨的Layer,所以如果把清除Cache的指令分開來執行,這樣就沒辦法清到上個指令的東西。 正確寫法:
FROM python:3.9
# 使用單一 RUN 指令完成所有動作
RUN apt-get update && \
apt-get install -y git && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
除此之外,Docker對Layer也有Cache機制,只要內容不變,就不會重新build該Layer,因此Best Practice是越不容易變化的Layer或指令要寫在越前面,或是也可以用.dockerignore忽略掉像是node_modules .git之類的東西。