跳轉到內容

Docker 多階段構建與鏡像優化實戰教程 2026

Docker 多階段構建與鏡像優化

一個未經優化的 Docker 鏡像體積往往高達 1-2GB,而經過合理優化後通常可以縮小到 100-300MB,有時甚至能壓縮 90% 以上。鏡像越小意味著:

  • 更快的部署速度
  • 📦 **更小的磁盤佔用
  • 🔒 更小的攻擊面

本文將帶你從 Docker 多階段構建的基礎語法開始,一步一步掌握鏡像優化的全部核心技術。


一、為什麼需要多階段構建?

1.1 單階段構建的問題

讓我們先看一個典型的 Node.js 項目的 Dockerfile:

dockerfile
FROM node:22

WORKDIR /app
COPY . .

RUN npm install
RUN npm run build

EXPOSE 3000
CMD ["node", "dist/main.js"]

問題在哪裡?

項目體積問題
基礎鏡像 node:22約 1.2 GB
npm 依賴的編譯工具gcc、g++、make
源代碼文件整個項目源碼
node_modules 開發依賴devDependencies
最終鏡像大小約 1.5 GB+

真正運行應用時,我們只需要:

  1. ✅ 編譯後的代碼 dist/
  2. ✅ 運行時依賴 dependencies
  3. ❌ 不需要:Node.js 完整工具鏈、源碼、編譯器、開發依賴

1.2 多階段構建的核心思想

"在一個鏡像中編譯,在另一個鏡像中只複製產物"

Stage 1: BUILD       →       Stage 2: RUN
┌──────────────┐       ┌──────────────┐
│ node:22      │ COPY  │ node:22-slim │
│ + 源代碼     │ ─── → │ + 編譯後代碼 │
│ + npm install│       │ + 運行時依賴│
│ + npm run    │       │ 僅運行產物    │
│ build        │       │              │
└──────────────┘       └──────────────┘
  ~1.5GB                  ~200MB

二、多階段構建基礎語法

2.1 第一個多階段 Dockerfile

dockerfile
# ---------- 階段一:構建階段 ----------
FROM node:22 AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev

COPY . .
RUN npm run build

# ---------- 階段二:運行階段 ----------
FROM node:22-slim

WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package.json ./

EXPOSE 3000
CMD ["node", "dist/main.js"]

效果對比:

鏡像體積
單階段版本~1.5 GB
多階段版本~200 MB
優化率約 87%

2.2 多階段命名與引用

dockerfile
# 使用 AS 命名階段
FROM golang:1.22-alpine AS build-stage
# ...

# 從命名階段複製文件
FROM alpine:3.20
COPY --from=build-stage /app/server /usr/local/bin/

# 也可以直接用階段序號(從 0 開始
COPY --from=0 /app/output /app/

# 甚至可以從外部鏡像複製文件
COPY --from=nginx:alpine /etc/nginx/nginx.conf /etc/nginx/

2.3 三階段構建實戰(更極致的優化)

dockerfile
# ============================================================
# 階段 1:依賴安裝層(專門用於緩存依賴
# ============================================================
FROM node:22-alpine AS deps

WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev

# ============================================================
# 階段 2:構建層
# ============================================================
FROM node:22-alpine AS builder

WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

# ============================================================
# 階段 3:運行層
# ============================================================
FROM node:22-alpine AS runner

WORKDIR /app

# 只複製必要的文件
COPY --from=builder /app/dist ./dist
COPY --from=deps /app/node_modules ./node_modules
COPY package.json ./

ENV NODE_ENV=production
USER node

EXPOSE 3000
CMD ["node", "dist/main.js"]

三、鏡像優化七大核心技術

3.1 選擇更小的基礎鏡像

鏡像大小適用場景
ubuntu:24.04~100 MB需要完整 Linux 環境
debian:12-slim~80 MB更輕量的 Debian
alpine:3.20~7 MB極致輕量,但使用 musl libc
gcr.io/distroless/static~2 MB空殼鏡像,僅含運行庫
scratch0 MB最基礎鏡像

不同語言的基礎鏡像選擇:

語言推薦基礎鏡像
Gogolang:alpinescratchdistroless/static
Node.jsnode:alpinenode:slim
Pythonpython:slimpython:alpine
Javaeclipse-temurin:jre-alpine
Rustrust:alpinescratch

3.2 使用 .dockerignore

.dockerignore 文件能顯著減少構建上下文大小:

dockerignore
# 版本控制
.git
.gitignore
.gitattributes

# Node.js
node_modules
npm-debug.log
yarn-error.log

# Python
__pycache__
*.pyc
.venv
venv

# 日誌
logs
*.log
npm-debug.log*

# 測試與文檔
test
tests
*.test.js
*.spec.ts
docs
README.md

# 本地配置
.env
.env.local
.env.*.local
*.local

# 編輯器
.vscode
.idea
*.swp

# 操作系統
.DS_Store
Thumbs.db

# 構建產物(在鏡像內重新構建
dist
build
*.exe

# CI/CD
.github
.gitlab-ci.yml
.travis.yml
.docker-compose.yml

3.3 合理利用構建緩存

利用 Docker 的緩存機制,可以大幅縮短重新構建時間。

緩存匹配規則:

  1. Docker 逐行檢查 Dockerfile 指令
  2. 如果指令和文件內容未變,則使用緩存
  3. 文件內容通過 checksum 校驗

最佳實踐:

dockerfile
# ❌ 不好:每次源碼變更都會觸發重新安裝依賴
COPY . .
RUN npm install

# ✅ 好:先複製依賴描述文件,利用緩存
COPY package*.json ./
RUN npm install
COPY . .

緩存效率對比:

方式代碼變更時是否重新安裝依賴
COPY . . 再 install✅ 每次都會
先複製 package.json 再 install❌ 僅當 package.json 變更

3.4 合併 RUN 指令

dockerfile
# ❌ 不好:每層 RUN 都是獨立的鏡像層
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y wget
RUN rm -rf /var/lib/apt/lists/*

# ✅ 好:合併成一條 RUN,減少層數
RUN apt-get update && \
    apt-get install -y \
        curl \
        wget \
        ca-certificates && \
    rm -rf /var/lib/apt/lists/*

3.5 清理緩存與臨時文件

dockerfile
# ✅ 安裝後立即清理緩存
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        curl \
        ca-certificates && \
    rm -rf /var/lib/apt/lists/* && \
    apt-get clean

# ✅ npm/yarn 安裝後清理緩存
RUN npm ci --omit=dev && npm cache clean --force

# ✅ pip 安裝後清理緩存
RUN pip install --no-cache-dir -r requirements.txt

3.6 使用非 root 用戶

dockerfile
# ✅ 創建專用用戶運行應用
FROM node:22-alpine

WORKDIR /app
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nextjs -u 1001

COPY --from=builder /app ./
USER nextjs

EXPOSE 3000
CMD ["node", "server.js"]

3.7 合理的鏡像層數

Docker 鏡像層原理:

操作創建新層
FROM
RUN
COPY
ADD
ENV❌ 元數據
CMD❌ 元數據
EXPOSE❌ 元數據

實戰建議:

  • 儘量合併同類操作以減少層數
  • 但不要為了減少層數而犧牲可讀性
  • 現代 Docker 的層數限制已經放寬到 127 層

四、實戰案例:Go 應用優化從 1.2GB 到 10MB

4.1 原始版本(1.2 GB)

dockerfile
FROM golang:1.22

WORKDIR /app
COPY . .
RUN go mod download && go build -o server .

EXPOSE 8080
CMD ["./server"]
組成部分體積
golang 基礎鏡像~1 GB
源碼與依賴~200 MB
總計~1.2 GB

4.2 多階段構建版本(25 MB)

dockerfile
# 構建階段
FROM golang:1.22-alpine AS builder

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o server .

# 運行階段
FROM alpine:3.20

WORKDIR /app
COPY --from=builder /app/server .

EXPOSE 8080
CMD ["./server"]

體積對比:

版本鏡像體積優化率
原始單階段~1.2 GB0%
多階段構建~25 MB~98%

4.3 極致優化:scratch 版本(10 MB)

dockerfile
# 構建階段
FROM golang:1.22-alpine AS builder

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build \
    -ldflags="-s -w -extldflags '-static'" \
    -o server .

# 運行階段:使用 scratch(空鏡像)
FROM scratch

WORKDIR /app

# 複製 ca-certificates 以便支持 HTTPS
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app/server .

EXPOSE 8080
CMD ["./server"]
版本鏡像體積優化率
原始單階段~1.2 GB0%
多階段 alpine~25 MB~98%
scratch 版本~10 MB~99%

4.4 Go 編譯參數詳解

go
// 關鍵編譯參數
CGO_ENABLED=0
// 禁用 CGO,使編譯產物可以在空鏡像中運行
// 缺點:某些依賴 CGO 的庫無法使用

GOOS=linux
// 指定目標操作系統

go build -ldflags="-s -w"
// -s: 移除符號表(調試信息)
// -w: 移除 DWARF 調試信息
// 兩者合用可減少 ~30% 二進制體積

五、實戰案例:Node.js 應用優化

5.1 完整最佳實踐 Dockerfile

dockerfile
# ============================================================
# Stage 1: 依賴層
# ============================================================
FROM node:22-alpine AS deps

WORKDIR /app

# 僅複製依賴描述文件
COPY package*.json ./
COPY .npmrc ./

# 只安裝生產依賴,不保存緩存
RUN npm ci --omit=dev --ignore-scripts

# ============================================================
# Stage 2: 構建層
# ============================================================
FROM node:22-alpine AS builder

WORKDIR /app

# 從 deps 階段複製已安裝的生產依賴
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# 構建應用
RUN npm run build

# ============================================================
# Stage 3: 運行層(最小化)
# ============================================================
FROM node:22-alpine AS runner

WORKDIR /app

# 設置生產環境
ENV NODE_ENV=production
ENV PORT=3000

# 創建非 root 用戶
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001

# 複製必要文件
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./

# 修改文件所有權
RUN chown -R nodejs:nodejs /app

# 切換到非 root 用戶
USER nodejs

EXPOSE 3000

CMD ["node", "dist/main.js"]

5.2 Node.js 優化技巧

bash
# ✅ 使用 npm ci 代替 npm install
# 基於 package-lock.json 精確安裝,速度更快
npm ci --omit=dev

# ✅ 只安裝生產依賴
npm install --omit=dev

# ✅ 使用 .dockerignore 排除 node_modules
# 讓 Dockerfile 中 COPY 不復制本地 node_modules

# ❌ 不要這樣做
npm install
# 會同時安裝 devDependencies

六、實戰案例:Python 應用優化

6.1 Python 多階段 Dockerfile

dockerfile
# ============================================================
# 構建階段
# ============================================================
FROM python:3.12-slim AS builder

WORKDIR /app

# 安裝編譯依賴(僅在構建階段需要)
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        build-essential \
        gcc \
        && \
    rm -rf /var/lib/apt/lists/*

# 創建虛擬環境
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# ============================================================
# 運行階段
# ============================================================
FROM python:3.12-slim

WORKDIR /app

# 複製虛擬環境
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

# 複製應用代碼
COPY . .

# 創建非 root 用戶
RUN useradd -m appuser && \
    chown -R appuser:appuser /app
USER appuser

EXPOSE 8000
CMD ["python", "app.py"]

6.2 Python 優化要點

技巧效果
使用 --no-cache-dir不保存 pip 緩存
使用虛擬環境複製隔離構建環境
使用 slim 鏡像減少基礎鏡像體積
--no-install-recommends不安裝推薦依賴

七、高級:distroless 鏡像詳解

7.1 什麼是 distroless 鏡像?

distroless 鏡像是由 Google 維護的一類極簡鏡像,它們:

  • ✅ 只包含應用及其運行時依賴
  • ✅ 不包含包管理器、shell 等工具
  • ✅ 鏡像體積極小
  • 🔒 攻擊面更小

7.2 使用 distroless 鏡像

dockerfile
# Go 應用
FROM golang:1.22 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o server .

FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/server /
CMD ["/server"]
dockerfile
# Node.js 應用
FROM node:22 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM gcr.io/distroless/nodejs22-debian12
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
CMD ["dist/main.js"]

7.3 distroless vs Alpine vs Slim

指標node:slimnode:alpinegcr.io/distroless/nodejs
體積~200 MB~130 MB~110 MB
shell
包管理器
安全性⭐⭐⭐⭐⭐⭐⭐⭐⭐
調試便利⭐⭐⭐⭐⭐⭐⭐⭐⭐

八、鏡像體積分析與診斷工具

8.1 docker history

查看鏡像各層體積分佈:

bash
docker history your-image:latest

輸出示例:

IMAGE          CREATED        CREATED BY                                      SIZE      COMMENT
abc123         1 hour ago     CMD ["node" "dist/main.js"]                    0B        buildkit.dockerfile.v0
<missing>      1 hour ago     COPY ./dist ./dist # buildkit                 5.2MB     buildkit.dockerfile.v0
<missing>      1 hour ago     COPY ./node_modules ./node_modules # buildkit  150MB     buildkit.dockerfile.v0
<missing>      1 week ago     /bin/sh -c #(nop)  CMD ["node"]               0B

8.2 dive:可視化鏡像層分析

bash
# 安裝 dive
wget https://github.com/wagoodman/dive/releases/download/v0.12.0/dive_0.12.0_linux_amd64.deb
sudo dpkg -i dive_0.12.0_linux_amd64.deb

# 分析鏡像
dive your-image:latest

dive 可以:

  • 查看每層的文件內容
  • 標記出低效的文件複製
  • 計算鏡像效率分數

8.3 docker-slim:自動優化鏡像

bash
# 安裝
curl -sL https://raw.githubusercontent.com/slimtoolkit/slim/master/scripts/install-slim.sh | sudo -E bash

# 自動優化
docker-slim build your-image:latest

九、Dockerfile 最佳實踐速查表

✅ Do

dockerfile
# 使用具體的標籤
FROM node:22-alpine3.20

# 每個容器只做一件事
# 一個容器只運行一個進程

# 最小化層數
# 合併同類命令

# 使用非 root 用戶
USER appuser

# 利用緩存
COPY package*.json ./
RUN npm install
COPY . .

# 多階段構建
FROM builder AS build
# ...
FROM runtime
COPY --from=build /app/output /app

❌ Don't

dockerfile
# ❌ 使用 latest 標籤
FROM node:latest

# ❌ 安裝不必要的包
RUN apt-get install -y vim nano curl wget ...

# ❌ 在鏡像中保存構建緩存
RUN npm install  # 不清空緩存

# ❌ 暴露不必要的端口
EXPOSE 22 80 443 3000 8080 9000 ...

# ❌ root 用戶運行
USER root

十、常見問題與排錯

Q1:musl libc vs glibc 兼容性問題

問題:使用 alpine 鏡像編譯的 Go 程序在某些平臺上無法運行。

解決方案:

dockerfile
# 確保使用 CGO_ENABLED=0 靜態編譯
RUN CGO_ENABLED=0 go build -o app .

# 或者使用 debian 鏡像作為構建環境
FROM golang:1.22 AS builder
# ...

# 或者動態鏈接時使用相同 libc 版本
FROM debian:12-slim
COPY --from=builder /app/app /

Q2:node_modules 跨平臺問題

問題:Mac/Windows 上的 node_modules 無法在 Linux 容器中直接使用。

解決方案:

dockerfile
# 始終在容器內安裝依賴
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .

Q3:COPY --from 路徑錯誤

問題:找不到源文件。

檢查清單:

  1. 文件是否真的在源階段存在
  2. 路徑是否正確(WORKDIR 影響相對路徑)
  3. 文件大小是否為 0(編譯失敗但沒報錯)
dockerfile
# 調試技巧:在構建階段添加 RUN ls
RUN ls -la /app/

Q4:構建緩存失效

問題:明明文件沒變卻不使用緩存。

檢查:

  • COPY . . 會導致任何文件變更都使緩存失效
  • 調整順序:先複製不易變的文件,再複製易變的文件
  • 檢查 .dockerignore 中是否遺漏了某些文件

結語

Docker 鏡像優化的核心可以用三句話總結:

  1. 使用多階段構建,把構建和運行分離
  2. 選擇最小化的基礎鏡像,安裝最少的依賴
  3. 合理利用緩存,讓重複構建更快

掌握這些技術後,你通常能把鏡像體積壓縮 50-99%,顯著提升部署速度和安全性。

下一步建議:


🐳 提示: 優化是一個持續的過程,定期檢查鏡像體積和層數,保持 Dockerfile 的整潔和高效。


延伸阅读

免责声明

本文仅供技术交流和学习参考。涉及第三方服务的链接可能包含 sponsored 标记,请自行核实服务条款、价格和可用性,并遵守当地法律法规。

最後更新於: