Docker Image Layer 詳解

深入理解 Docker Image 的分層架構、Layer 類型、快取機制和優化技巧


目錄


Image 基本概念

Image 是什麼?

Image = 唯讀的模板
      = 用來建立 Container 的藍圖
      = 包含應用程式 + 依賴 + 設定

Image 的組成

┌─────────────────────┐
│   Application       │  ← 你的應用程式
├─────────────────────┤
│   Dependencies      │  ← 依賴套件
├─────────────────────┤
│   Runtime           │  ← Node.js, Python 等
├─────────────────────┤
│   Libraries         │  ← 系統函式庫
├─────────────────────┤
│   Base OS           │  ← Ubuntu, Alpine 等
└─────────────────────┘

Layer(層)的概念

Image 由多層組成,每層都是唯讀的

FROM ubuntu:20.04          ← 第 1 層
RUN apt-get update         ← 第 2 層
RUN apt-get install nginx  ← 第 3 層
COPY app.js /app/          ← 第 4 層

優點:
✅ 共享層次,節省空間
✅ 快取機制,加速建立
✅ 增量更新

Layer 的類型與產生方式

Docker Image 的每一層都是由 Dockerfile 的指令產生的。不同指令會產生不同類型的 Layer。

產生 Layer 的指令(會增加層數)

FROM ubuntu:20.04          # Layer 1: 基礎層
RUN apt-get update         # Layer 2: 系統更新
RUN apt-get install nginx  # Layer 3: 安裝軟體
COPY app.js /app/          # Layer 4: 複製檔案
ADD archive.tar.gz /data/  # Layer 5: 添加檔案(並解壓)

不產生 Layer 的指令(只修改 metadata)

ENV NODE_ENV=production    # 不產生層:只設定環境變數
WORKDIR /app               # 不產生層:只改變工作目錄
EXPOSE 8080                # 不產生層:只聲明 port
CMD ["npm", "start"]       # 不產生層:只設定預設指令
ENTRYPOINT ["node"]        # 不產生層:只設定入口點
LABEL version="1.0"        # 不產生層:只添加標籤
USER appuser               # 不產生層:只切換使用者
VOLUME /data               # 不產生層:只聲明掛載點
ARG BUILD_DATE             # 不產生層:只定義建構參數

各種 Layer 的詳細說明

1. FROM Layer(基礎層)

FROM ubuntu:20.04
# 或
FROM node:18-alpine
# 或
FROM scratch    # 完全空白(用於極小化 Image)

特點

  • 每個 Dockerfile 的第一層
  • 引入整個基礎 Image 的所有層
  • 可以使用多階段建構(Multi-stage build)

實際組成

FROM ubuntu:20.04 實際上包含:
├── Layer 1: 核心檔案系統 (rootfs)
├── Layer 2: 基本系統套件 (apt, bash, etc)
├── Layer 3: 系統函式庫 (libc, libssl, etc)
└── Metadata: 環境變數、預設指令等

2. RUN Layer(執行指令層)

# Shell 形式(會啟動 /bin/sh)
RUN apt-get update

# Exec 形式(直接執行,不啟動 shell)
RUN ["apt-get", "update"]

# 合併多個指令(減少層數)
RUN apt-get update && \
    apt-get install -y nginx && \
    apt-get clean

產生的內容

  • 指令執行後的檔案系統變更
  • 新增、修改、刪除的檔案
  • 套件安裝產生的檔案

最佳實踐

# ❌ 不好:每個指令一層(3 層)
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get clean

# ✅ 好:合併指令(1 層)
RUN apt-get update && \
    apt-get install -y curl && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# ✅ 更好:多行清晰格式
RUN apt-get update && \
    apt-get install -y \
        curl \
        vim \
        git \
        wget && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

Layer 大小陷阱

# ❌ 錯誤:分開刪除檔案不會減少 Layer 大小
RUN wget https://example.com/bigfile.tar.gz  # Layer: +500MB
RUN rm bigfile.tar.gz                        # Layer: +0MB(但 bigfile 仍在上一層)
# 最終 Image 大小:500MB

# ✅ 正確:在同一層刪除
RUN wget https://example.com/bigfile.tar.gz && \
    tar xzf bigfile.tar.gz && \
    rm bigfile.tar.gz                        # Layer: +50MB(解壓後的檔案)
# 最終 Image 大小:50MB

3. COPY Layer(複製檔案層)

# 基本用法
COPY app.js /app/
COPY package*.json ./
COPY src/ /app/src/

# 複製並改變擁有者
COPY --chown=appuser:appgroup app.js /app/

# 從特定建構階段複製(多階段建構)
COPY --from=builder /app/dist /app/dist

產生的內容

  • 從建構 context 複製的檔案
  • 檔案的 metadata(權限、時間戳記)

快取機制

# ✅ 利用 Docker 快取優化
COPY package*.json ./      # 只在 package.json 改變時重建
RUN npm install            # 依賴很少改變,快取命中率高
COPY . .                   # 程式碼經常改變,放在最後

# ❌ 不好的順序
COPY . .                   # 任何檔案改變都會重建
RUN npm install            # 每次都重裝依賴(浪費時間)

4. ADD Layer(添加檔案層)

# 基本複製(與 COPY 相同)
ADD app.js /app/

# 自動解壓縮 tar 檔案
ADD archive.tar.gz /data/     # 自動解壓到 /data/

# 從 URL 下載(不推薦)
ADD https://example.com/file.tar.gz /data/

ADD vs COPY 比較

功能 COPY ADD
複製本地檔案
自動解壓縮 tar
從 URL 下載
透明度 ✅ 高 ⚠️ 低
推薦使用 ⚠️ 特殊情況

最佳實踐

# ✅ 推薦:明確使用 COPY
COPY package.json .

# ✅ 需要解壓時才用 ADD
ADD app.tar.gz /app/

# ❌ 不推薦:用 ADD 下載(應該用 RUN + wget/curl)
ADD https://example.com/file.tar.gz /data/

# ✅ 正確:用 RUN 下載
RUN wget https://example.com/file.tar.gz && \
    tar xzf file.tar.gz && \
    rm file.tar.gz

5. 多階段建構的 Layer

# 第一階段:建構環境(包含所有建構工具)
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install               # 安裝所有依賴(含開發依賴)
COPY . .
RUN npm run build             # 建構應用程式
# 此階段產生的 Layer:~500MB

# 第二階段:運行環境(只包含必要檔案)
FROM node:18-alpine           # 更小的基礎 Image
WORKDIR /app
COPY --from=builder /app/dist ./dist          # 只複製建構結果
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/main.js"]
# 最終 Image 大小:~150MB

# 優點:
# ✅ 建構工具不包含在最終 Image
# ✅ 最終 Image 更小、更安全
# ✅ 建構過程仍有完整工具

Layer 的實際結構

查看 Image 的 Layer

# 查看 Image 歷史(所有層)
docker history nginx:latest

# 輸出範例:
IMAGE          CREATED       CREATED BY                                      SIZE
a6bd71f48f68   2 weeks ago   /bin/sh -c #(nop)  CMD ["nginx" "-g" "daem…    0B
<missing>      2 weeks ago   /bin/sh -c #(nop)  EXPOSE 80                   0B
<missing>      2 weeks ago   /bin/sh -c #(nop)  STOPSIGNAL SIGQUIT          0B
<missing>      2 weeks ago   /bin/sh -c #(nop)  CMD ["nginx" "-g" "daem…    0B
<missing>      2 weeks ago   /bin/sh -c #(nop)  ENTRYPOINT ["/docker-en…    0B
<missing>      2 weeks ago   /bin/sh -c #(nop) COPY file:e57eef017a414…     4.62kB
<missing>      2 weeks ago   /bin/sh -c set -x     && groupadd --syst…      112MB    ← RUN 產生的層
<missing>      2 weeks ago   /bin/sh -c #(nop)  ENV PKG_RELEASE=1~buster    0B
<missing>      2 weeks ago   /bin/sh -c #(nop)  ENV NJS_VERSION=0.7.0       0B
<missing>      2 weeks ago   /bin/sh -c #(nop)  ENV NGINX_VERSION=1.21.5    0B
<missing>      2 weeks ago   /bin/sh -c #(nop)  LABEL maintainer=NGINX…     0B
<missing>      2 weeks ago   /bin/sh -c #(nop)  CMD ["bash"]                0B
<missing>      2 weeks ago   /bin/sh -c #(nop) ADD file:09675d11695f65…     80.4MB   ← FROM 基礎層

詳細檢查 Layer 內容

# 匯出 Image 為 tar
docker save nginx:latest -o nginx.tar

# 解壓查看結構
tar -xf nginx.tar

# 目錄結構:
nginx/
├── manifest.json              # Image 清單
├── config.json               # Image 設定
├── layer1_hash/
│   ├── layer.tar             # 第 1 層的檔案
│   └── json                  # 層的 metadata
├── layer2_hash/
│   ├── layer.tar             # 第 2 層的檔案
│   └── json
└── ...

Layer 的實作機制

1. Union File System(聯合檔案系統)

Container 的檔案系統視圖:

┌─────────────────────────────────┐
│  Container Layer (讀寫層)        │  ← 執行時的修改存在這裡
├─────────────────────────────────┤
│  Layer 4: COPY app.js           │  ← 唯讀
├─────────────────────────────────┤
│  Layer 3: RUN apt install nginx │  ← 唯讀
├─────────────────────────────────┤
│  Layer 2: RUN apt-get update    │  ← 唯讀
├─────────────────────────────────┤
│  Layer 1: FROM ubuntu:20.04     │  ← 唯讀
└─────────────────────────────────┘

實際實作:OverlayFS (Linux)
主機檔案系統:
/var/lib/docker/overlay2/
├── layer1_id/
│   └── diff/           # 實際的檔案系統變更
├── layer2_id/
│   ├── diff/
│   └── lower           # 指向下層
├── layer3_id/
│   ├── diff/
│   └── lower
└── ...

2. Copy-on-Write(寫時複製)機制

讀取檔案的流程:
1. 查找 Container Layer
2. 如果找不到,往下層找
3. 一直找到最底層
4. 回傳第一個找到的檔案

修改檔案的流程:
1. 查找檔案在哪一層
2. 如果在 Image Layer(唯讀):
   ├── 複製檔案到 Container Layer
   └── 在 Container Layer 修改
3. 如果已在 Container Layer:
   └── 直接修改

範例:
修改 /etc/nginx/nginx.conf
├── 原檔案在 Layer 3(唯讀)
├── 複製到 Container Layer
├── 在 Container Layer 修改
└── 之後讀取都從 Container Layer

刪除檔案:
刪除 /var/log/app.log
├── 原檔案在 Layer 2
├── 在 Container Layer 建立 whiteout 標記
└── 之後查找會看到 whiteout,回報檔案不存在

Layer 快取機制

快取規則

FROM ubuntu:20.04              # Cache: 檢查基礎 Image 是否相同
RUN apt-get update             # Cache: 檢查指令文字是否相同
COPY package.json .            # Cache: 檢查檔案內容 checksum
RUN npm install                # Cache: 前一層命中才檢查此層
COPY . .                       # Cache: 檢查所有檔案的 checksum

快取失效規則:
✅ 指令文字改變 → 快取失效
✅ COPY/ADD 的檔案改變 → 快取失效
✅ FROM 的基礎 Image 更新 → 快取失效
❌ RUN 執行的外部資源改變 → 快取不會失效(陷阱!)

快取陷阱與解決

# ❌ 問題:總是快取(即使外部資源更新)
RUN wget https://example.com/app.zip && \
    unzip app.zip
# Docker 只檢查指令文字,不知道 app.zip 已更新

# ✅ 解決 1:使用 ARG 強制失效
ARG CACHE_BUST=1
RUN wget https://example.com/app.zip && \
    unzip app.zip
# 建構時:docker build --build-arg CACHE_BUST=$(date +%s) .

# ✅ 解決 2:使用 --no-cache
docker build --no-cache -t myapp .

# ✅ 解決 3:指定版本
RUN wget https://example.com/app-v1.2.3.zip && \
    unzip app-v1.2.3.zip

Layer 優化技巧

1. 減少 Layer 數量

# ❌ 不好:太多 Layer(5 層)
FROM ubuntu:20.04
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git
RUN apt-get install -y vim
RUN apt-get clean

# ✅ 好:合併指令(1 層)
FROM ubuntu:20.04
RUN apt-get update && \
    apt-get install -y \
        curl \
        git \
        vim && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

2. 優化 Layer 順序(利用快取)

# ❌ 不好:任何程式碼改變都重裝依賴
FROM node:18
WORKDIR /app
COPY . .                    # 程式碼經常變
RUN npm install             # 每次都重裝

# ✅ 好:依賴很少變,快取命中率高
FROM node:18
WORKDIR /app
COPY package*.json ./       # 只在依賴改變時重建
RUN npm install             # 大部分時間快取命中
COPY . .                    # 程式碼變化放最後

3. 使用 .dockerignore 減少 COPY Layer 大小

# .dockerignore
node_modules
.git
.env
*.log
.DS_Store
__pycache__
*.pyc
.pytest_cache
dist/
build/
*.md
.gitignore
Dockerfile
docker-compose.yml

效果:
- 減少建構 context 大小
- 加快 COPY 速度
- 減少 Layer 大小

4. 多階段建構減少最終 Image 大小

# 第一階段:建構(可以很大)
FROM golang:1.19 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o myapp
# 此階段:~800MB

# 第二階段:運行(極小化)
FROM alpine:3.17
RUN apk --no-cache add ca-certificates
COPY --from=builder /app/myapp /usr/local/bin/
CMD ["myapp"]
# 最終 Image:~20MB(相比 800MB 節省 97.5%)

5. 清理暫存檔案(同一層)

# ❌ 錯誤:分層刪除不會減少大小
RUN apt-get update                      # Layer 1: +50MB
RUN apt-get install -y build-tools      # Layer 2: +500MB
RUN apt-get clean                       # Layer 3: +0MB(但 Layer 2 仍是 500MB)
# 總大小:550MB

# ✅ 正確:同層清理
RUN apt-get update && \
    apt-get install -y build-tools && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*         # Layer 1: +450MB
# 總大小:450MB


實戰範例

範例 1:Python 應用程式

# 多階段建構
FROM python:3.11 AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

FROM python:3.11-slim
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY . .
ENV PATH=/root/.local/bin:$PATH
CMD ["python", "app.py"]

# Layer 組成:
# Stage 1 (builder): ~1GB(包含編譯工具)
# Stage 2 (final):  ~200MB(只有運行時)

範例 2:Node.js 應用程式

FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && \
    npm cache clean --force

FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]

# Layer 優化:
# 1. npm ci 比 npm install 更快、更可靠
# 2. --only=production 不安裝開發依賴
# 3. npm cache clean 清理快取
# 4. 多階段確保最終 Image 乾淨

範例 3:靜態網站(Nginx)

# 建構階段
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# 運行階段
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

# 最終 Image:
# - 不包含 Node.js
# - 不包含 node_modules
# - 不包含原始碼
# - 只有建構後的靜態檔案 + Nginx
# 大小:~20MB(相比包含 Node 的 ~500MB)


最佳實踐

1. 減少 Layer 數量

# ❌ 不好:每個指令一層
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/*

2. 利用快取機制

# ❌ 不好:程式碼改變導致重裝依賴
COPY . .
RUN npm install

# ✅ 好:先複製依賴檔案
COPY package*.json ./
RUN npm install
COPY . .

3. 清理暫存檔案(同一層)

# ❌ 錯誤:分層刪除不會減少大小
RUN wget https://example.com/bigfile.tar.gz
RUN rm bigfile.tar.gz

# ✅ 正確:同層清理
RUN wget https://example.com/bigfile.tar.gz && \
    tar xzf bigfile.tar.gz && \
    rm bigfile.tar.gz

常見問題

Q1: 為什麼刪除檔案 Layer 大小沒變?

A: 因為檔案在上一層已經存在。刪除指令只是在新層標記檔案為刪除,原檔案仍佔用空間。解決方法:在同一層下載、解壓、刪除。

Q2: 如何查看 Image 有哪些 Layer?

A: 使用 docker history <image> 查看所有層和大小。

Q3: 多階段建構有什麼好處?

A: 最終 Image 只包含執行時需要的檔案,不包含建構工具,可大幅減少 Image 大小(通常減少 80-90%)。

Q4: 快取何時會失效?

A:

  • 指令文字改變
  • COPY/ADD 的檔案內容改變
  • FROM 基礎 Image 更新
  • 使用 --no-cache 建構

Image 命名規則

[registry]/[namespace]/[repository]:[tag]

範例:
docker.io/library/nginx:latest
│         │       │      │
│         │       │      └─ 標籤(版本)
│         │       └──────── 倉庫名稱
│         └──────────────── 命名空間(使用者/組織)
└────────────────────────── Registry 位址

簡寫:
nginx          = docker.io/library/nginx:latest
nginx:1.21     = docker.io/library/nginx:1.21
user/app       = docker.io/user/app:latest

總結

核心要點

  • Image 由多層唯讀 Layer 組成,使用聯合檔案系統
  • FROM、RUN、COPY、ADD 會產生新 Layer
  • 利用快取機制可加速建構,先複製依賴檔案
  • 合併 RUN 指令、在同一層清理暫存檔案可減少 Layer 大小
  • 多階段建構可大幅減少最終 Image 大小

快速參考

操作 最佳做法 原因
安裝依賴 先 COPY 依賴檔案,後 COPY 程式碼 利用快取
執行指令 && 合併多個 RUN 減少 Layer
刪除檔案 在同一層刪除 減少大小
建構方式 使用多階段建構 最小化 Image
基礎選擇 alpine > slim > 完整版 更小更快

建立日期:2025-11-10 最後更新:2025-11-18

🔗相關文章