使用 Multi-stage Build 高效建構輕量化 Docker 映像
前言:
在軟體開發的流程中,打包應用程式是一個不可或缺的環節,通常需要經歷下載依賴、編譯程式碼、打包應用,然後執行的步驟。傳統的 Dockerfile 會將所有這些步驟集中在同一個建構過程中,雖然操作簡單直觀,但也伴隨著一些潛在的問題。今天,我們要介紹 Docker Multi-stage Build 這個強大工具,看看它如何幫助我們優化建構流程並提升應用部署的效率。
認識 Docker Multi-stage Build
傳統 Dockerfile 的挑戰
在傳統的 Dockerfile 中,我們建構映像的過程通常包含所有步驟,從下載依賴到編譯應用程式。舉個例子,這是一個常見的 Dockerfile:
FROM golang:1.16.0-alpine3.13
WORKDIR /app
# 複製 go module 和 sum 檔案並下載依賴
COPY go.mod go.sum ./
RUN go mod download
# 複製所有原始碼
COPY . .
# 執行程式碼檢查、 測試及編譯
RUN go vet ./... && go test ./... && go build -o /app/server
CMD ["/app/server"]
這樣的 Dockerfile 執行了所有步驟,包括下載依賴、程式碼檢查、測試和編譯。而在映像建構完成後,所有這些步驟的產物,甚至是編譯器、開發工具和測試用的依賴,通通都會被保留下來,導致映像非常龐大。
這樣的巨大映像檔會造成幾個問題。首先,映像越大,部署速度就越慢,尤其是當你需要頻繁更新應用程式時。其次,映像中包含不必要的工具和依賴,增加了安全風險和潛在的攻擊面。
Docker Multi-stage Build 的解決方案
為了應對映像肥大的問題,Docker 在 17.05 版本引入了 Multi-stage Build。這種技術讓我們能夠在一個 Dockerfile 中定義多個建構階段,每個階段可以有不同的用途和配置。舉例來說,我們可以在第一個階段進行編譯和打包的過程,然後在第二個階段中,僅複製編譯後的可執行檔到最終的映像中,並且只保留執行應用所需的文件。
這種方法能顯著縮減映像的大小,因為不必要的開發工具和依賴都不會被打包進最終的映像。Multi-stage Build 尤其適合建構步驟複雜、依賴繁多的應用程式,如 Go、C/C++ 或 Java 等語言開發的專案。我們可以先在前期階段進行編譯,然後只將編譯完成的二進位檔案放入最終映像,這樣不僅能減少映像的大小,還能提升安全性。
Multi-stage Build 的基本語法
多次使用 FROM
指令
在 Multi-stage Build 中,我們可以在同一個 Dockerfile 中多次使用 FROM
指令來定義不同的建構階段。每個階段都會有自己的基底映像和工作環境,用來完成不同的任務。例如:
# 第一階段:建構階段
FROM golang:1.16.0-alpine3.13 AS builder
WORKDIR /workspace
# 安裝所有依賴
COPY go.mod go.sum ./
RUN go mod download
# 複製所有原始碼並進行建構
COPY . .
RUN go build -o server
# 第二階段:運行階段
FROM alpine:latest
WORKDIR /app
# 從 builder 階段複製建構好的應用程式
COPY --from=builder /workspace/server .
# 設定容器的啟動指令
CMD ["./server"]
在這個範例中,我們將建構步驟放在第一個階段,並使用 golang:1.16.0-alpine3.13
作為基底映像,這個階段被命名為 builder
。接著,在第二個階段中,我們選擇使用較小的 alpine
基底映像,來建立一個精簡的運行環境。這種做法可以顯著減少最終映像的大小,因為最終映像只包含了執行應用所需的文件,而不包含建構工具和開發依賴。
使用 COPY --from
指令
COPY --from=<stage>
指令允許我們從之前的建構階段中,將文件複製到當前階段。例如,在上述範例中,COPY --from=builder /workspace/server .
的作用是把第一階段中建構好的應用程式從 builder
複製到運行階段。這樣,我們就能確保最終映像中只保留執行應用的必要檔案,避免因打包多餘的開發工具和依賴而增大映像大小。
Multi-stage Build 的實際應用範例
範例:Node.js 應用的 Multi-stage Build
讓我們來看一下如何使用 Multi-stage Build 來優化一個 Node.js 應用。假設我們有一個使用 Express 框架的應用程式,其目錄結構如下:
my-node-app/
├── package.json
├── package-lock.json
└── app.js
傳統的 Dockerfile 可能長這樣:
FROM node:14
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["node", "app.js"]
這種寫法的問題在於,生成的映像會非常龐大,因為 node:14
映像本身包含了許多開發工具和不必要的套件。使用 Multi-stage Build 可以顯著減少最終映像的大小,以下是改進後的寫法:
# 第一階段:建構階段
FROM node:14 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
# 第二階段:運行階段
FROM node:14-slim
WORKDIR /app
COPY --from=builder /app .
CMD ["node", "app.js"]
在這個範例中,我們先在 builder
階段中安裝所有的依賴和打包應用,然後使用更精簡的 node:14-slim
作為運行階段的基底映像。node:14-slim
比 node:14
輕量許多,只包含運行應用所需的最基本組件,這樣可以顯著減少映像的大小並加速部署。