目錄
- Image 基本概念
- Layer 的類型與產生方式
- 各種 Layer 的詳細說明
- Layer 的實際結構
- Layer 的實作機制
- Layer 快取機制
- Layer 優化技巧
- Layer 的實際應用範例
- Image 命名規則
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 app.js /app/
# 從特定建構階段複製(多階段建構)
COPY /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 /app/dist ./dist # 只複製建構結果
COPY /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 /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 /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 /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 /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