macOS 上的 Docker VM 細節#

當在 macOS 上執行 docker run alpine 時,背後其實發生了一連串「跨虛擬機器」的事情。這一章解釋為什麼 macOS 上的 Docker 必須有一個隱形的 Linux VM,以及如何進入該 VM 內部觀察。

為什麼 macOS 上的 Docker 需要 VM#

容器(Container)的本質是 Linux Kernel 提供的隔離機制:命名空間(Namespaces)、控制群組(cgroups)、聯合檔案系統(Union File System,例如 OverlayFS)。這些機制屬於 Linux Kernel 的特性,不存在於 macOS 的 Darwin / XNU Kernel 中。

因此:

  • macOS 沒有 Linux Kernel
  • 沒有 Linux Kernel 就沒有 Linux Namespaces 與 cgroups
  • 沒有這些機制就無法執行 Linux 容器

解決方法只有一個:在 macOS 上跑一個 Linux VM,把 Docker daemon 安裝在 VM 裡,然後讓 macOS 上的 docker CLI 透過 socket 與 VM 內的 daemon 溝通。使用者只看到 docker 指令,看不到背後的 VM。

Hypervisor 的選擇#

macOS 上的 Docker VM 過去與現在用到的 Hypervisor 包含:

  • HyperKit:Docker Desktop 早期使用,基於 macOS 內建的 Hypervisor.framework
  • Apple Virtualization.framework:較新版本 Docker Desktop 在 Apple Silicon 上採用
  • qemu:Colima、Lima 等開源替代方案常用,可跨架構模擬
  • krunkit / vfkit:部分新工具使用的輕量化 VM monitor

不同方案的取捨主要在於效能、相容性、與 macOS 版本的整合度。

LinuxKit VM#

Docker Desktop 內建的 Linux VM 並不是完整的發行版(不是 Ubuntu、不是 Alpine 桌面版),而是由 Docker 維護的 LinuxKit 客製化映像:

  • 體積極小(數十 MB)
  • 只含必要的 Linux Kernel 與 systemd-free 的 init 機制
  • 預先安裝 Docker daemon
  • 啟動極快

這個 VM 的使命單純:盡可能透明地承載 Docker daemon。

用戶端與伺服器端的分工#

在 macOS 上執行 docker version 會看到兩段資訊:

docker version

預期輸出(具體值依環境而異):

  • Client 區塊顯示 macOS 上的 CLI 版本,OS/Archdarwin/arm64darwin/amd64
  • Server 區塊顯示 VM 內的 daemon 版本,OS/Archlinux/arm64linux/amd64

兩者透過 Unix Domain Socket(macOS 端)或 vsock(VM 端)橋接。

進入 LinuxKit VM 內觀察#

由於 VM 是隱形的,預設不能直接 ssh 進去。以下幾種方法可以進入 VM:

方法 1:docker debug(Docker Desktop 內建)#

近期版本的 Docker Desktop 提供 docker debug 指令,能直接進入 VM 或容器:

# 進入特定容器
docker debug <container-id>

# 部分版本支援進入 VM 本身
docker debug --host

docker debug 會掛載一個包含常用工具(curl、ps、vim 等)的 sidecar,省去自製 debug 容器的麻煩。

方法 2:特權容器掛載 host PID Namespace#

最經典、最跨版本可行的方法是啟動一個特權容器並掛載 host(也就是 LinuxKit VM)的 PID Namespace:

docker run -it --rm --privileged --pid=host alpine nsenter -t 1 -m -u -n -i sh

這條指令的意義:

  • --privileged:給予容器幾乎完整的權限
  • --pid=host:共用 LinuxKit VM 的 PID Namespace
  • nsenter -t 1 -m -u -n -i sh:進入 PID 1(也就是 VM 的 init process)的所有命名空間

進入後執行 uname -aps -efls /var/lib/docker 都是 VM 視角的結果。

方法 3:nerdctl(containerd CLI)#

如果使用 containerd 為基底的環境(例如 Rancher Desktop、Lima、Colima),可以用 nerdctl 取代 docker

nerdctl ps
nerdctl run -it alpine sh

nerdctl 直接與 containerd 對話,較能反映底層真實狀態。

方法 4:Colima#

Colima 是 macOS 上的開源 Docker / Kubernetes runtime。它使用 Lima 啟動 QEMU VM,並提供 SSH 進入 VM 的能力:

# 啟動
colima start

# SSH 進 VM
colima ssh

# 在 VM 內查看
uname -a
ps -ef | head

對於想清楚看到 VM 內部結構的學習者,Colima 是非常友善的選擇。

VM 視角下的容器#

進入 VM 後可以驗證:「容器其實只是 host(VM)上的行程(Process)」。例如先在 host 跑:

docker run -d --name web nginx

再進入 VM:

docker run -it --rm --privileged --pid=host alpine nsenter -t 1 -m -u -n -i sh
ps -ef | grep nginx

預期輸出(具體值依環境而異):可以看到屬於 nginx 的 master process 與 worker process,PID 是 VM 觀點下的編號,與容器內的 PID 1 不同。

容器內的 PID 1 是隔離後的視角(PID Namespace),但在 VM host 視角下,這個行程有完全不同的 PID。這個對應關係是後續章節會深入探討的重點。

在 Linux 上沒有這層 VM#

對應地,在 Linux host 上直接安裝 Docker Engine 時:

  • 沒有 Hypervisor、沒有 LinuxKit VM
  • daemon 直接跑在 host kernel 上
  • 容器即 host 行程,ps -ef 在 host 上就看得到

這也是為什麼學習容器原理時,建議在 Linux 環境下實驗:少一層 VM,觀察更直接。

延伸閱讀#