2024年6月18日
從 Docker 換到 containerd + nerdctl:架構差異、安裝 runc 與 CNI,以及移除 Docker 後的網路問題
為什麼不用 Docker 了
以前部署容器,第一步一定是裝 Docker。但這次換掉它有兩個原因:
- Docker 太肥大了:這些主機只需要「拉映像檔、跑容器」,Docker 卻附帶了 dockerd 常駐程式、自己的網路與儲存管理、Swarm 等一整套功能,多一層元件就多一份要維護、要更新、要擔心的東西
- 和公司的 Harbor 不相容:公司的 Harbor 一直沒有升版,在我們的環境裡,安裝 docker-ce 18.06 以上的版本就無法從 Harbor 拉取映像檔。但為了配合舊的 Harbor 一直停在舊版 Docker,也不是長久之計
而 Docker 底層真正在管理容器的,其實就是 containerd。Kubernetes 從 1.24 開始移除了 dockershim,也已經直接使用 containerd 這類 CRI 執行環境。所以只要把 Docker 拿掉、直接用 containerd,再搭配一個指令和 Docker 幾乎一樣的 CLI——nerdctl——就能用更少的元件做到一樣的事。
Docker 和 containerd 是什麼關係
一個 docker run 背後其實經過好幾層:
| 元件 | 負責什麼 |
|---|---|
| docker CLI | 使用者下指令的工具 |
| dockerd | Docker 的常駐程式,處理 API、建置映像檔、管理網路與 Volume |
| containerd | 管理容器的生命週期:拉映像檔、管理快照、建立與監控容器 |
| containerd-shim | 每個容器一個,讓 containerd 重啟時容器不會跟著停 |
| runc | 真正呼叫 Linux 核心(namespace、cgroups)建立容器的低階執行環境,遵循 OCI 規範 |
| CNI plugins | 負責容器的網路:建立橋接網卡、分配 IP、設定 Port 對應 |
改用 containerd + nerdctl 之後,dockerd 這一層就不見了,nerdctl 直接呼叫 containerd。容器的建立方式完全一樣,還是 containerd-shim 加 runc;差別在於網路:Docker 的網路是 dockerd 自己管的,containerd 本身不管網路,所以要另外安裝 CNI plugins,由 nerdctl 呼叫它們來建立網路。
這也是安裝時需要三樣東西的原因:
- containerd:管理容器
- runc:建立容器
- CNI plugins:容器網路
和 containerd 互動的三種 CLI
containerd 有三種常見的命令列工具,用途不同:
| 工具 | 定位 | 適合 |
|---|---|---|
ctr | containerd 內建的除錯工具 | 低階除錯,指令和 Docker 差很多,也不處理網路 |
crictl | 透過 CRI 介面操作,是給 Kubernetes 用的 | 在 K8s 節點上查看 kubelet 建立的 Pod 與容器 |
nerdctl | 和 Docker CLI 相容的工具 | 取代 docker 指令,支援 run -p、compose、login 等 |
日常使用選 nerdctl 就對了:docker run 改成 nerdctl run,大部分的參數都一樣,連 nerdctl compose up 都可以直接吃原本的 docker-compose.yml。
安裝 containerd
加入 Docker 的 apt 套件庫
containerd 的 containerd.io 套件是由 Docker 的 apt 套件庫提供的,先加入金鑰與套件庫:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
安裝 containerd
只安裝 containerd.io,不要安裝 docker-ce:
sudo apt-get update
sudo apt-get install containerd.io
產生設定檔並啟動
containerd.io 附的設定檔是給 Docker 用的精簡版,建議改用 containerd 的完整預設設定,之後要調整 Registry、cgroup 等設定才有地方改:
sudo mkdir -p /etc/containerd
containerd config default | sudo tee /etc/containerd/config.toml > /dev/null
sudo systemctl daemon-reload
sudo systemctl enable --now containerd
sudo systemctl status containerd
安裝 runc
containerd.io 套件其實已經附帶了 runc,可以先確認:
runc --version
如果是從 GitHub 下載 containerd 的二進位檔安裝(不透過 apt),或是需要更新 runc 版本,再手動安裝。先確認系統架構:
dpkg --print-architecture # 例如 amd64、arm64
再到 runc Releases 選擇版本下載:
RUNC_VERSION=1.1.12
ARCH=$(dpkg --print-architecture)
wget https://github.com/opencontainers/runc/releases/download/v${RUNC_VERSION}/runc.${ARCH}
sudo install -m 755 runc.${ARCH} /usr/local/sbin/runc
runc --version
Releases 頁面上也會列出
-rc結尾的預覽版本(例如v1.2.0-rc.1),正式環境請選擇沒有-rc的穩定版。
安裝 CNI plugins
containerd 不管網路,nerdctl run -p 8080:80 這種 Port 對應、容器之間的網路,都是由 CNI plugins 處理。containerd.io 沒有附帶 CNI,一定要自己安裝,到 CNI plugins Releases 選擇版本:
CNI_VERSION=1.5.0
ARCH=$(dpkg --print-architecture)
wget https://github.com/containernetworking/plugins/releases/download/v${CNI_VERSION}/cni-plugins-linux-${ARCH}-v${CNI_VERSION}.tgz
sudo mkdir -p /opt/cni/bin
sudo tar Cxzvf /opt/cni/bin cni-plugins-linux-${ARCH}-v${CNI_VERSION}.tgz
/opt/cni/bin 是 nerdctl 預設尋找 CNI plugins 的位置。裡面會有 bridge(建立橋接網卡)、host-local(分配 IP)、portmap(Port 對應)、firewall 等執行檔,nerdctl 建立容器時會依序呼叫它們。
安裝 nerdctl
到 nerdctl Releases 下載。nerdctl 提供兩種版本:
nerdctl-x.y.z-linux-amd64.tar.gz:只有 nerdctl 本身,containerd、runc、CNI 要自己裝(也就是本篇的做法)nerdctl-full-x.y.z-linux-amd64.tar.gz:一次附帶 containerd、runc、CNI plugins、BuildKit 等所有相依元件,適合全新的主機一次裝好
NERDCTL_VERSION=1.7.6
ARCH=$(dpkg --print-architecture)
wget https://github.com/containerd/nerdctl/releases/download/v${NERDCTL_VERSION}/nerdctl-${NERDCTL_VERSION}-linux-${ARCH}.tar.gz
sudo tar Cxzvf /usr/local/bin nerdctl-${NERDCTL_VERSION}-linux-${ARCH}.tar.gz nerdctl
nerdctl version
nerdctl version 同時顯示 Client 與 Server(containerd)的版本,兩者都有出現代表 nerdctl 已經成功連上 containerd。
驗證
sudo nerdctl run -d --name web -p 8080:80 nginx:alpine
sudo nerdctl ps
curl http://localhost:8080
連線公司的 Harbor
登入
和 Docker 一樣:
sudo nerdctl login harbor.example.com
sudo nerdctl pull harbor.example.com/realty/house-api:1.3.0
自簽憑證
公司內部的 Harbor 常用自簽憑證,拉取時會出現 x509: certificate signed by unknown authority。nerdctl 會讀取 /etc/containerd/certs.d/ 底下的設定,把公司的 CA 憑證放進對應的資料夾:
sudo mkdir -p /etc/containerd/certs.d/harbor.example.com
sudo cp company-ca.crt /etc/containerd/certs.d/harbor.example.com/ca.crt
# /etc/containerd/certs.d/harbor.example.com/hosts.toml
server = "https://harbor.example.com"
[host."https://harbor.example.com"]
ca = "/etc/containerd/certs.d/harbor.example.com/ca.crt"
測試環境如果只是暫時要略過驗證,可以加 --insecure-registry,但正式環境不要這樣用。
和 Docker 不一樣的地方
nerdctl 雖然和 Docker CLI 相容,還是有幾個地方容易卡住:
- containerd 有 Namespace:nerdctl 預設使用
default這個 Namespace,而 Kubernetes 的 kubelet 使用k8s.io。在 K8s 節點上用nerdctl pull拉下來的映像檔,kubelet 看不到;要查看 K8s 建立的容器,也要加--namespace k8s.io(或-n k8s.io) nerdctl build需要 BuildKit:nerdctl 本身不會建置映像檔,要另外安裝並啟動buildkitd(nerdctl-full版本有附)。這幾台主機只負責執行容器、建置交給 CI,所以沒有安裝- 沒有常駐程式也代表沒有自動重啟管理員:
--restart=always是由 nerdctl 搭配 containerd 的機制處理,主機重開機後要確認容器有自動起來 - 預設需要 root:上面的指令都加了
sudo。nerdctl 也支援 Rootless 模式,但需要額外設定 - 網卡名稱不同:Docker 的預設橋接網卡是
docker0,nerdctl 是nerdctl0,網段也不一樣。防火牆規則、監控設定如果有寫死docker0要一起改
錯誤紀錄:移除 Docker 後外部連不進容器
症狀
主機原本裝過 Docker,移除後改用 containerd + nerdctl。nerdctl run -p 8080:80 啟動容器後:
- 在主機上
curl localhost:8080正常 - 從外部其他機器連
http://主機IP:8080卻一直沒有回應,最後逾時
相同的問題在 nerdctl 的 GitHub 也有人回報:containerd/nerdctl#2142。
原因
apt remove docker-ce 只會移除程式,Docker 在系統上建立的 iptables 規則與 docker0 網卡不會一起清掉。Docker 會在 nat 表裡建立自己的 DOCKER 鏈,並在 PREROUTING、OUTPUT 中插入規則指向它;nerdctl 透過 CNI 的 portmap 外掛建立的則是 CNI-HOSTPORT-DNAT 這組規則。
外部連進來的封包會先經過 PREROUTING,Docker 殘留的規則仍然排在前面,干擾了原本應該由 CNI 處理的轉送;而從主機本身用 localhost 連線,走的是另一條路徑,所以本機測試一切正常。這也是這個問題很難發現的原因:本機 curl 沒問題,只有從外面連才會出事。
解決辦法
清除 Docker 殘留的 iptables 規則與網卡。
⚠️ 修改 iptables 前,先確認自己不會把 SSH 連線一起擋掉,並且最好在有主控台可以登入的情況下操作。
1. 先備份目前的規則
sudo iptables-save > ~/iptables-backup-$(date +%Y%m%d).rules
2. 找出 Docker 相關的規則
# 列出 nat 表的規則與編號
sudo iptables -t nat -L -n --line-numbers
# 也可以直接篩選出和 Docker 有關的規則
sudo iptables-save | grep -i docker
3. 刪除規則
先刪除 PREROUTING、OUTPUT、POSTROUTING 中指向 DOCKER 的規則(用上一步看到的編號),再清空並刪除 DOCKER 鏈本身:
# 刪除指定編號的規則,例如 PREROUTING 的第 1 條(編號依實際情況)
sudo iptables -t nat -D PREROUTING 1
# 清空 DOCKER 鏈內的規則
sudo iptables -t nat -F DOCKER
# 沒有任何規則再引用它之後,才能刪除這條鏈
sudo iptables -t nat -X DOCKER
刪除一條規則後,後面規則的編號會往前移,所以每刪一條就重新列一次,或是從編號大的開始刪,避免刪錯(參考 iptables: Index of deletion too big)。filter 表中的 DOCKER、DOCKER-USER、DOCKER-ISOLATION-STAGE-1/2 等鏈也用同樣的方式處理。
4. 刪除 docker0 網卡
ip link show # 找到 docker0
sudo ip link set docker0 down
sudo ip link delete docker0
(舊的做法是 ifconfig docker0 down,但 ifconfig 在新版的 Ubuntu 已經不是預設安裝,建議改用 ip 指令;參考 How do I delete virtual interface in Linux。)
5. 重新建立容器並從外部驗證
sudo nerdctl rm -f web
sudo nerdctl run -d --name web -p 8080:80 nginx:alpine
# 確認 CNI 的 Port 對應規則存在
sudo iptables -t nat -L CNI-HOSTPORT-DNAT -n
# 從另一台機器測試
curl http://<主機IP>:8080
如果主機有用 iptables-persistent 之類的工具把規則存起來,開機時會自動載回來,記得清理後重新儲存(sudo netfilter-persistent save),不然重開機問題又回來了。
避免再發生
- 新主機直接裝 containerd + nerdctl,不要先裝 Docker 再移除
- 舊主機要換掉 Docker 時,把「清理 iptables 與
docker0」寫進移轉步驟,而不是等出問題才處理 - 驗收時一定要從外部機器測試,不能只在主機上
curl localhost
參考資料
一句話總結
Docker 底層本來就是 containerd,拿掉 dockerd 之後,只要自己補上建立容器的 runc 與負責網路的 CNI plugins,再用和 Docker 指令相容的 nerdctl 操作,就能用更少的元件跑一樣的容器;但從 Docker 換過來的主機,記得清掉 Docker 殘留的 iptables 規則與 docker0,而且驗收時一定要從外部連線測試。