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/Arch為darwin/arm64或darwin/amd64Server區塊顯示 VM 內的 daemon 版本,OS/Arch為linux/arm64或linux/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 --hostdocker 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 Namespacensenter -t 1 -m -u -n -i sh:進入 PID 1(也就是 VM 的 init process)的所有命名空間
進入後執行 uname -a、ps -ef、ls /var/lib/docker 都是 VM 視角的結果。
方法 3:nerdctl(containerd CLI)#
如果使用 containerd 為基底的環境(例如 Rancher Desktop、Lima、Colima),可以用 nerdctl 取代 docker:
nerdctl ps
nerdctl run -it alpine shnerdctl 直接與 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,觀察更直接。
延伸閱讀#
- Docker Docs — Docker Desktop architecture: https://docs.docker.com/desktop/ ↗
- LinuxKit GitHub: https://github.com/linuxkit/linuxkit ↗
- Colima GitHub: https://github.com/abiosoft/colima ↗
- Linux man pages —
nsenter(1)、namespaces(7)