從 GraphDriver 開始看#

當 container 啟動後,可以用 docker inspect 看 runtime 配給它的儲存目錄結構:

docker run -d --name demo alpine:3.19 sleep 3600
docker inspect -f '{{json .GraphDriver.Data}}' demo

預期可以看到四個 key:

  • LowerDir:image 的所有 read-only layer,以冒號串接(最頂層在最前)
  • UpperDir:container 啟動後新增的可寫層,所有寫入都會落在這裡
  • MergedDir:上面兩者透過 OverlayFS 聯合掛載出來的「container 看到的根檔案系統」
  • WorkDir:OverlayFS 自己用的暫存目錄(atomic rename 等需要)

container 內部的 / 對應的就是 MergedDir。從 host 直接 ls 那個目錄,會看到完整的檔案樹。

在大多數 Linux 安裝中,這些目錄位於 /var/lib/docker/overlay2/<id>/ 之下。Docker Desktop(Mac/Windows)由於跑在 VM 內,host 上看不到,需要進到 VM 才能觀察。

UpperDir 與 LowerDir 的對比#

幾個值得記住的對應關係:

  • 在 container 內讀檔:OverlayFS 從 MergedDir 讀,命中 LowerDir 或 UpperDir 都行
  • 在 container 內寫檔:先把檔案從 LowerDir 拷貝一份到 UpperDir(Copy-On-Write),再對 UpperDir 的副本寫入
  • 在 container 內刪檔:UpperDir 會新增一個 whiteout 標記,告訴 OverlayFS「這個檔案在合併視圖中要視為不存在」
  • LowerDir 永遠不會被改寫,這是 image 可以被多 container 共用的前提

修改 UpperDir 觀察行為#

來做一個小實驗(需要 root 權限觀察 host 上的目錄):

docker run -d --name demo alpine:3.19 sleep 3600
docker exec demo sh -c 'echo "from container" > /a.txt'

# 取得 UpperDir
UPPER=$(docker inspect -f '{{.GraphDriver.Data.UpperDir}}' demo)
ls "$UPPER"
cat "$UPPER/a.txt"

預期觀察:

  • UpperDir 下會出現 a.txt,內容就是 container 寫入的字串
  • 在 host 上修改 UpperDir 內這個檔案,container 內 cat /a.txt 也會跟著變(因為它們其實是同一個 inode,OverlayFS 只是把它「呈現」在合併檔案系統的 /a.txt
  • 如果在 host 上把 UpperDir 的某個檔案刪除,container 內也會看不到

直接動 UpperDir 是「打開引擎蓋」的觀察手段,正式環境不要這樣操作;OverlayFS 對某些操作有原子性要求,繞過 runtime 直接寫可能造成不一致。

把 UpperDir 凍結成新 image:commit#

實驗到一個段落,可以把目前的可寫層固化成新 image:

docker commit demo demo:after-experiment
docker history demo:after-experiment
docker inspect -f '{{json .RootFS.Layers}}' demo:after-experiment

預期可觀察:

  • RootFS.Layers 會比原本 alpine 多一層
  • 多出來的那層 SHA256,對應的就是「UpperDir 在 commit 當下的內容」
  • 拿這個新 image 起 container,會是一個全新的 UpperDir(空的),原本 UpperDir 的內容已經沉到 LowerDir 裡,變成 read-only

這個流程把抽象的 layer 概念變得具體:commit = 把 UpperDir 凍結 → 生成一個新的 read-only layer → 加到 LowerDir 鏈的最頂端。

LowerDir 的順序為什麼重要#

LowerDir 是用冒號串接的多個目錄,順序代表「優先級」:愈左邊的層愈接近最終視圖的「上面」。當同樣路徑的檔案在多層存在時,OverlayFS 取最上面那層的版本。這也是「後加入的 layer 可以覆蓋早期 layer 中的檔案」的實作機制。

從實驗回到觀念#

整個實驗串起來的觀念是:

  • container 的根檔案系統不是真正存在某一處的「一份檔案樹」,而是多層目錄聯合掛載出來的視圖
  • 寫入永遠落在最上層(UpperDir),底層 image 永遠安全
  • commit 把這個「最上層」物化成 image 的一層

下一節進到 OverlayFS 本身,看 Linux kernel 是怎麼把這件事實作出來的。

延伸閱讀#