Docker 多階段建置優化映像檔

Docker Multi-Stage Build for Image Optimization

多階段建置概述與優勢

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

參考資料

comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy