多階段建置概述與優勢
Docker 多階段建置(Multi-Stage Build)是 Docker 17.05 版本引入的重要功能,它允許在單一 Dockerfile 中使用多個 FROM 指令,每個 FROM 指令都會開始一個新的建置階段。這項功能的主要優勢包括:
- 減小映像檔大小:最終映像檔只包含執行應用程式所需的檔案,不包含建置工具和中間產物
- 簡化建置流程:不再需要維護多個 Dockerfile 或複雜的建置腳本
- 提升安全性:減少攻擊面,因為最終映像檔不包含編譯器、套件管理器等工具
- 加速 CI/CD 流程:更小的映像檔意味著更快的推送和拉取速度
傳統 Dockerfile 的問題
在多階段建置出現之前,開發者通常會面臨以下困境:
1
2
3
4
5
6
7
8
9
| # 傳統的單階段 Dockerfile
FROM golang:1.21
WORKDIR /app
COPY . .
RUN go mod download
RUN go build -o main .
CMD ["./main"]
|
這種方式產生的映像檔會包含:
- 完整的 Go 編譯環境(約 800MB)
- 所有原始碼
- 下載的依賴套件
- 編譯過程中的中間檔案
最終映像檔可能高達 1GB 以上,但實際執行只需要一個幾 MB 的二進位檔案。
多階段建置基本語法
多階段建置的核心概念是使用多個 FROM 指令,並透過 COPY --from 從前面的階段複製所需檔案:
1
2
3
4
5
6
7
8
9
10
11
| # 第一階段:建置階段
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN go build -o main .
# 第二階段:執行階段
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/main .
CMD ["./main"]
|
實際範例:Go 應用程式
以下是一個完整的 Go 應用程式多階段建置範例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| # 建置階段
FROM golang:1.21-alpine AS builder
# 安裝必要的建置工具
RUN apk add --no-cache git ca-certificates
WORKDIR /src
# 先複製 go.mod 和 go.sum 以利用快取
COPY go.mod go.sum ./
RUN go mod download
# 複製原始碼並建置
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /app/server ./cmd/server
# 執行階段
FROM scratch
# 複製 CA 憑證(用於 HTTPS 請求)
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# 複製執行檔
COPY --from=builder /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]
|
大小比較:
- 傳統方式:約 1.2GB
- 多階段建置:約 10-15MB
實際範例:Node.js 應用程式
Node.js 應用程式也能從多階段建置中獲益:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| # 階段一:安裝依賴
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production
# 階段二:建置應用程式
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# 階段三:執行階段
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
# 建立非 root 使用者
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 appuser
# 只複製必要檔案
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
USER appuser
EXPOSE 3000
CMD ["node", "dist/index.js"]
|
使用 FROM … AS 命名階段
為建置階段命名可以提高 Dockerfile 的可讀性和維護性:
1
2
3
4
5
6
7
8
9
10
11
12
| FROM maven:3.9-eclipse-temurin-17 AS compile
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package -DskipTests
FROM eclipse-temurin:17-jre-alpine AS runtime
WORKDIR /app
COPY --from=compile /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
|
命名階段的好處:
- 清楚表達每個階段的用途
- 方便在
COPY --from 中引用 - 支援使用
--target 建置特定階段
從特定階段複製檔案
COPY --from 不僅可以從前面的階段複製,還可以從外部映像檔複製:
1
2
3
4
5
6
7
8
9
10
| FROM alpine:latest
# 從前面的階段複製
COPY --from=builder /app/binary /usr/local/bin/
# 從外部映像檔複製
COPY --from=nginx:alpine /etc/nginx/nginx.conf /etc/nginx/
# 從指定索引的階段複製(0 表示第一個階段)
COPY --from=0 /compiled/app /app
|
使用 –target 建置特定階段
--target 參數允許只建置 Dockerfile 中的特定階段,這對於開發和測試非常有用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| FROM node:20-alpine AS base
WORKDIR /app
COPY package*.json ./
FROM base AS development
RUN npm install
COPY . .
CMD ["npm", "run", "dev"]
FROM base AS test
RUN npm ci
COPY . .
CMD ["npm", "test"]
FROM base AS production
RUN npm ci --only=production
COPY . .
CMD ["npm", "start"]
|
建置不同階段的指令:
1
2
3
4
5
6
7
8
| # 建置開發環境映像檔
docker build --target development -t myapp:dev .
# 建置測試環境映像檔
docker build --target test -t myapp:test .
# 建置正式環境映像檔
docker build --target production -t myapp:prod .
|
最佳實踐
1. 選擇適當的基礎映像檔
1
2
3
4
5
6
7
| # 建置階段使用完整映像檔
FROM golang:1.21 AS builder
# 執行階段使用最小映像檔
FROM scratch # 完全空白(適合靜態連結的二進位檔)
FROM alpine:latest # 約 5MB(適合需要基本工具的情況)
FROM distroless/base # Google 的最小化映像檔
|
2. 善用建置快取
將不常變動的指令放在前面:
1
2
3
4
5
6
7
8
9
10
| FROM node:20-alpine AS builder
WORKDIR /app
# 先複製套件定義檔(較少變動)
COPY package.json package-lock.json ./
RUN npm ci
# 再複製原始碼(經常變動)
COPY . .
RUN npm run build
|
3. 使用 .dockerignore
建立 .dockerignore 檔案排除不必要的檔案:
1
2
3
4
5
6
7
| node_modules
.git
.gitignore
*.md
Dockerfile
docker-compose.yml
.env*
|
4. 最小化層數
合併相關的 RUN 指令:
1
2
3
4
5
6
7
8
9
10
| # 較差的做法
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get clean
# 較好的做法
RUN apt-get update && \
apt-get install -y curl && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
|
5. 使用非 root 使用者
1
2
3
| FROM node:20-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
|
參考資料