上一章解決了「容器是什麼」,這一章解決「容器的檔案系統是怎麼來的」。
這是 Docker 設計裡最漂亮的一段。理解分層之後,一堆看似無關的現象會突然全部串起來:為什麼 pull 有好幾條進度條、為什麼第二次 pull 快得像作弊、為什麼容器啟動只要幾毫秒、為什麼容器刪掉資料就消失、為什麼 Dockerfile 改一行就要重建一大半、以及一個很多人不知道的資安地雷——你在映像裡刪掉的密碼檔,其實還在裡面。
你一定看過這個畫面:
$ docker pull nginx
Using default tag: latest
latest: Pulling from library/nginx
a2abf6c4d29d: Pull complete
a9edb18cadd1: Pull complete
589b7251471a: Pull complete
186b1aaa4aa6: Pull complete
b4df32aa5a72: Pull complete
a0bcbecc962e: Pull complete
Digest: sha256:0d17b565c37b...
Status: Downloaded newer image for nginx:latest六條進度條、六個奇怪的 ID。映像不是一個檔案,它是好幾個「層(layer)」組成的。
接著你 pull 另一個也是基於 Debian 的映像:
$ docker pull redis
a2abf6c4d29d: Already exists ← 這層跳過了!
7c3f1b4c9e88: Pull complete
...Already exists——因為 nginx 和 redis 都建立在同一個 Debian 基底上,那一層你已經有了,不用再下載,而且兩個映像共用同一份。
接下來三張卡,把「層」到底是什麼、怎麼疊起來、怎麼被修改,一路拆到底。
FROM eclipse-temurin:17-jre ← 基底(本身也是好幾層)
RUN apt-get install -y curl ← 第 N 層:多了 curl 相關檔案
COPY app.jar /app/app.jar ← 第 N+1 層:多了一個 app.jar
CMD ["java", "-jar", "/app/app.jar"] ← 只是設定,不佔實體層app.jar 這一個檔案。層一旦建立就永遠不會被修改,因為它的身分證(雜湊值)是用內容算出來的——改內容就變成另一層了。這帶來兩個重要推論:
COPY 了一個含密碼的設定檔,第 5 層再 RUN rm 把它刪掉,那個密碼檔真的消失了嗎?答案在本章最後一張卡,這是很多人踩過的資安地雷。層各自獨立存在磁碟上,但容器裡的程式需要看到一個正常、完整的檔案系統。負責把它們疊起來的技術叫做聯合檔案系統(Union File System),Docker 現在預設用的實作是 Linux 核心內建的 OverlayFS。
/。容器看到的 / (merged)
↑ 疊合
[可寫層] /app/config.yml(改過的版本)← 蓋住下面那個
[映像層 3] /app/app.jar
[映像層 2] /usr/bin/curl
[映像層 1] /bin, /etc, /usr, /lib …(Debian 基底)pivot_root,換過去的那個目錄就是這裡的 merged。兩章在這裡接起來了:ch02 說「換掉容器的根目錄」,ch03 說「那個根目錄是好幾層疊出來的」。在 Linux 宿主機上執行 mount | grep overlay,會看到類似這樣的東西(有簡化):
overlay on /var/lib/docker/overlay2/abc.../merged type overlay
(lowerdir=/var/lib/docker/overlay2/l/A:/var/lib/docker/overlay2/l/B,
upperdir=/var/lib/docker/overlay2/abc.../diff,
workdir=/var/lib/docker/overlay2/abc.../work)沒有魔法,就是一個掛載參數。容器的檔案系統,是宿主機上一個 overlay 掛載點而已。
問題來了:映像的層是唯讀的,但容器裡的程式明明可以寫檔案。怎麼辦到的?
這層一開始是空的。之後所有的寫入都發生在這裡,下面的映像層永遠不動。
docker rm 掉,可寫層跟著刪除。映像層完全沒動過,所以重新開一個容器就是全新的乾淨狀態。(要保住資料,就要用 volume 把資料寫到容器外面,見 ch06。)mssql-data:/var/opt/mssql 不只是為了保存資料,也是為了效能。現在可以精確定義這兩個天天在用、卻常被混為一談的詞了。
image is being used by running container 錯誤的來源。docker commit 是什麼?→ 把容器目前的可寫層固化成一個新的映像層。可以用,但不建議——這等於用「手動改物件狀態」的方式產生 class,別人完全不知道你做了什麼。正道是寫進 Dockerfile。回到前面埋的問題。假設有人寫了這樣的 Dockerfile:
FROM eclipse-temurin:17-jre
COPY secrets.env /tmp/secrets.env ← 第 3 層:含資料庫密碼
RUN ./setup.sh && rm /tmp/secrets.env ← 第 4 層:用完就刪掉,安全了吧?
COPY app.jar /app/app.jar直覺上:檔案刪了,映像裡就沒有了。大錯特錯。
secrets.env,這件事已成定局。rm 只是加了一個 whiteout 標記:「這個路徑當作不存在」。它是遮住,不是抹除。docker save 匯出成 tar 檔解開,或用 docker history、dive 這類工具,第 3 層裡的密碼原封不動。RUN --mount=type=secret),它掛載進去但不會留在任何一層。.dockerignore 擋掉 .env、.git、金鑰檔,避免 COPY . . 時整包送進去(.git 裡藏著歷史上所有曾經 commit 過的秘密)。docker history <映像> 可以看到這個映像是怎麼一步步建出來的,是稽核與除錯的好工具。缺點是別人也看得到。| 面向 | Image(映像) | Container(容器) |
|---|---|---|
| 本質 | 一疊唯讀層 + 設定檔 | Image + 可寫層 + 執行中的 process |
| 狀態 | 靜態,躺在磁碟上不會變 | 動態,活著且會累積狀態 |
| 類比 | class / 母帶 / 食譜+食材 | object / 播放中 / 爐上那鍋 |
| 可否修改 | 不可變(改了就是新映像) | 可寫,但寫的東西只活在這個容器裡 |
| 數量關係 | 一份 | 同一映像可開 N 個,彼此互不影響 |
| 刪掉的後果 | 要重新 pull 或 build | 可寫層一併消失 → 沒放 volume 的資料就沒了 |
| 查看指令 | docker images / docker history | docker ps -a / docker inspect |
| 操作 | 實際發生的事 | 代價 | 推論出的現象 |
|---|---|---|---|
| 讀取 | 由上往下找,找到就讀(不複製) | 幾乎為零 | 多個容器共用映像層,省空間 |
| 修改 | 先把整個檔案複製到可寫層,再改複本(Copy-on-Write) | 第一次寫要付複製整檔的成本 | 大檔案/資料庫不該寫在可寫層 → 用 volume |
| 刪除 | 在可寫層放 whiteout 標記,遮住下層檔案 | 低 | 映像裡刪掉的秘密其實還在下層! |
| 面向 | 帶來什麼 | 背後原因 |
|---|---|---|
| 傳輸 | 只下載缺少的層(Already exists) | 層以內容雜湊為身分證,可精確比對 |
| 儲存 | 多個映像/容器共用同一層,磁碟只存一份 | 層不可變,共用絕對安全 |
| 啟動速度 | 毫秒級啟動 | 不複製映像,只加空的可寫層再掛載 |
| 建置速度 | 沒改到的層直接沿用快取(ch08 主題) | 層以內容決定身分,內容沒變就能重用 |
| 代價① | 寫大檔案第一次很慢 | Copy-on-Write 要先複製整個檔案 |
| 代價② | 刪不掉歷史:秘密會永久留在層裡 | 刪除只是 whiteout 遮蔽,不是抹除 |
| 代價③ | 層太多會變慢、變大 | 每層都有查找與儲存開銷(ch10 教怎麼減層) |
點擊卡片翻面查看答案,共 10 張。