為什麼要切成 Layer#

把整個檔案系統打成一個大 tarball 也能跑 container,但有兩個痛點:

  • 每改一行就要重傳整個檔案系統;網路成本高
  • 多個 image 之間若共用 base(例如同樣是 ubuntu),那塊內容會被重複儲存好幾份

Docker 的解法是把 image 切成 Layer。每一層只記錄「相對於下一層的差異」(檔案新增、修改、刪除)。這樣相同的 base layer 可以被多個 image 共用,傳輸時也只需要對 registry 補那幾層缺失的。

Layer 是 image 在 disk 上的儲存單位,也是 registry 傳輸的單位。每一層都有一個獨立的 SHA256 摘要,這就是 Content-Addressable Storage 的根基。

一行 Dockerfile,一層 Layer#

Dockerfile 中能改變檔案系統的指令(RUNCOPYADD),每一條都會產出一層新的 layer。其他純 metadata 指令(ENVLABELCMDEXPOSE)只改 image config,不會新增 filesystem layer,但仍會產生一個中間 image id。

舉例:

FROM alpine:3.19
RUN apk add --no-cache curl
COPY app.sh /usr/local/bin/app.sh
CMD ["/usr/local/bin/app.sh"]

build 完之後,可預期的 layer 結構大致是:

  • Layer 1:alpine 的 base 檔案系統
  • Layer 2:apk add curl 之後新增的二進位、library、index 檔
  • Layer 3:COPY app.sh 帶進來的單一檔案
  • CMD 不產生 filesystem layer,只寫進 image config

這個「一行一層」的特性有兩個直接後果:

  • 任何寫入動作都會被永久保留在那一層;後續層即使刪除,前面那層仍存在
  • 將多個相關指令合併成一個 RUN 可以減少層數、也避免「裝了又刪」的中間檔被留在 image 裡

共用 layer 的實際好處#

假設兩個 image 都從 python:3.11-slim 出發:

  • myapp-api:1.0:再加上 FastAPI、自家程式碼
  • myapp-worker:1.0:再加上 Celery、自家程式碼

下載這兩個 image 時,base 的 python:3.11-slim 各層只會被傳輸與儲存一次。docker pull 時可以看到「Already exists」的 layer,就是因為本機已經有這個 SHA256 對應的 blob。

手動產生新 layer:docker commit#

除了 Dockerfile,還可以直接從一個跑著的 container 產出新 image,這就是 commit:

docker run --name demo -it alpine:3.19 sh
# 在 container 內:
# touch /hello.txt
# echo "hi" > /hello.txt
# exit
docker commit demo demo:v1

預期行為:

  • 原本的 alpine base layer 不變
  • container 在執行時的所有寫入(在它的可寫 layer 內)會被「凍結」成一層新的 read-only layer
  • 新 image demo:v1 由 alpine 的若干層 + 這一層新的 diff 組成

docker history demo:v1 應該可以看到最頂端多了一層,CREATED BY 欄位通常是空的或 sh,因為它不是來自 Dockerfile 而是來自手動 commit。

docker commit 在實務上不建議當成 build 主流程:缺乏可重現性、沒有歷史記錄。它的價值主要在實驗、debug、或從既有 container 抽出狀態當 base。

Layer 的不可變性#

每一層 layer 一旦建立就是 read-only 的,且以 SHA256 識別。這帶來幾個重要性質:

  • 同一個 SHA256 的 layer 在 disk 上只會有一份
  • 改任何一個 byte,SHA256 就變,等於是不同的 layer
  • 因此 Dockerfile 中愈早期、愈穩定的指令應該放在愈前面,這樣後續修改才不會讓前面的 cache 失效

這解釋了一個常見的優化:把 COPY package.jsonRUN npm install 放在 COPY . . 之前。只要 package.json 沒變,npm install 那層的 cache 就能命中,build 時間大幅縮短。

層數不是愈少愈好,但不能爆掉#

舊版 Docker(aufs 時代)有層數上限(通常是 127)。overlay2 雖然提高了限制,但層數過多仍會:

  • 拉長 mount 時間
  • 增加 metadata 查詢成本
  • docker history 變得難讀

實務上,把同一個邏輯步驟(例如「安裝依賴 + 清理 cache」)合併成單一 RUN,是平衡可讀性與層數的常見做法。

延伸閱讀#