Docker:入門概念
從 OCI 規範與 Namespaces、Cgroups 等底層原理切入,比較 Docker 與 Podman 架構差異。
應用程式容器化是現代軟體開發不可或缺的技術。透過 Docker,可以將環境打包成映像檔 (Image)。而 Dockerfile 用來定義如何建置這個映像檔的藍圖。這篇文章紀錄我對 Dockerfile 的核心概念理解。
Open Container Initiative (OCI)
容器化技術最早源於 1979 年 Keith Tantlinger 所提出,這邊的容器是字面上的容器,也就是貨運上所看到的貨櫃。 直到 2000 年,FreeBSD Jails 被 FreeBSD 團隊提出,FreeBSD Jails 不僅隔離了檔案系統,還隔離了網路、使用者和行程,創造了一個更完整的「監獄」環境
容器化技術的成功可以歸功於 Namespaces (命名空間) Namespaces 負責隔離。它能讓容器內的行程擁有自己獨立的視圖,彷彿它在一個獨立的作業系統中運行。例如:
- PID namespace: 容器內的行程有自己獨立的行程編號 (PID 1)。
- NET namespace: 擁有獨立的網路設備、IP 地址和路由表。
- MNT namespace: 擁有獨立的掛載點和檔案系統。
近一步的發展要到 2006 年,Google 提出 Cgroups
Cgroups 負責 「資源限制與管理」 。它能限制一個容器可以使用的 CPU、記憶體、硬碟 I/O 等資源,確保容器之間不會互相搶佔資源,避免單一容器耗盡主機所有資源。
在 Docker 出現之前,靠著 Namespaces 以及 Cgroups 的操控,就可以達到容器化。缺點在於過程繁瑣及複雜。
而這也成為 Docker 成為現在主流的原因之一,Docker 在於它將複雜的底層技術抽象化,提供舒適的開發者體驗,讓容器化從少數系統管理員的專屬工具,變成了所有開發者都能輕易上手的技術。
OCI 規範
為了建立一個開放且標準化的容器生態系統,OCI 制定了三大核心規範,為容器的生命週期提供了清晰的定義,確保了不同工具之間的互通性。
執行階段規範 (Runtime Specification)
OCI 執行階段規範(runtime-spec)定義了容器該如何 「運行」。它詳細描述了一個被稱為「OCI Runtime Bundle」的標準檔案系統結構,其中必須包含:
config.json: 這是一個核心設定檔,裡面定義了容器的所有運行參數,例如要執行的命令、環境變數、掛載點、以及前面提到的 Namespaces 和 Cgroups 等資源限制。- 根檔案系統 (Root Filesystem): 一個目錄,包含了容器內部所需的所有檔案與目錄結構。
此規範確保了任何符合 OCI 標準的容器執行引擎(如 runc、crun)都能夠讀取這個 Bundle,並以相同且可預測的方式啟動和管理容器。這意味著,只要建立了一個符合規範的容器,它就可以在任何支援 OCI 的平台上(例如 Docker、Podman 或 Kubernetes)無縫運行。
映像檔規範 (Image Specification)
OCI 映像檔規範(image-spec)定義了容器映像檔的 「打包格式」。一個符合 OCI 標準的映像檔主要由以下幾個部分組成:
- 映像檔清單 (Image Manifest): 一個
JSON檔案,用來描述映像檔的整體結構,包含了對設定檔和各個檔案系統圖層 (Layers) 的引用(通常是透過其內容的雜湊值)。 - 映像檔設定檔 (Image Config): 一個
JSON檔案,包含了映像檔的元數據(Metadata),例如映像檔的作者、建立時間、以及預設的執行參數(如config.json的內容)。 - 檔案系統圖層 (Filesystem Layers): 一系列經過壓縮的檔案系統變更集(通常是 tar 檔案)。容器的根檔案系統是由這些圖層堆疊組合而成的,這種分層設計使得映像檔的儲存和傳輸更有效率。
這個規範讓不同的容器工具(例如 Docker、Buildah、Kaniko)能夠以標準化的方式建立、推送 (push) 和拉取 (pull) 映像檔,確保了映像檔在不同工具和平台之間的可攜性。
分發規範 (Distribution Specification)
OCI 分發規範(distribution-spec)定義了容器映像檔 「如何儲存與分發」 的標準 API。這個規範主要針對容器倉庫 (Container Registry) 的行為進行了標準化,例如:
- 如何透過 API 上傳 (push) 映像檔的各個圖層和清單。
- 如何透過 API 下載 (pull) 映像檔。
- 如何進行身份驗證與授權。
遵循此規範的容器倉庫(如 Docker Hub, Google Container Registry (GCR), Harbor)可以被任何符合標準的客戶端工具存取。這為容器映像檔的全球分發和共享提供了一個穩定、可靠的基礎設施。
Docker vs Podman
當我們整天講容器化,Docker 已經快變成容器化代名詞了,但我們必須知道容器化技術像前面所說,是遵循 OCI,透過 Namespaces 存取以及 Cgroups 控制,達成容器化。而 Docker 只是達成的其中一個手段。
再更深入了解 Docker 與其他工具的差別前,我們先看看 Docker 啟動時,他會在背景程序做哪些事情。
Docker Engine 的核心:Client-Server 架構
要理解 Docker,首先必須認識其核心——Docker Engine。它並不是一個單一的程式,這個架構主要由三個部分組成:
-
Docker 守護行程 (Daemon):
- 這是一個名為
dockerd的持續性背景程序,也是 Docker 的「大腦」與權力核心。 - 當系統開機後,
dockerd就會以root高權限啟動並在背景監聽。 - 它負責處理所有繁重的工作:管理映像檔、建立與執行容器、設定網路、掛載儲存卷等。基本上,所有與容器生命週期相關的操作都由它一手包辦。
- 這是一個名為
-
REST API:
dockerd會對外提供一個標準化的 REST API 接口。這套 API 定義了客戶端可以如何與守護行程溝通並命令它執行任務。
-
Docker 命令列介面 (CLI):
- 這是我們最熟悉的
docker指令。當你在終端機輸入docker run或docker ps時,dockerCLI 並不會自己去執行容器。 - 相反地,它會將指令打包成一個 API 請求,然後發送給在本機上運行的
dockerd。dockerd收到請求後,才會執行對應的操作,並將結果回傳給 CLI 顯示。
- 這是我們最熟悉的
這個 Client-Server 架構是 Docker 設計的基石,但也正是這個中央化的 dockerd 守護行程,成為了它與 Podman 最根本的區別。
Podman:無守護行程 (Daemonless) 架構
Podman (Pod Manager) 的出現,旨在提供一個更符合傳統 Linux/UNIX 哲學的容器管理工具。它最大的特點就是無守護行程 (Daemonless)。
Podman 認為,不需要一個永遠在背景以 root 權限運行的中央程序來管理容器。
- 當執行
podman run時,Podman 會直接從 Shell 程序中建立一個子程序來執行容器。 - 這個過程與執行
ls或cp等任何其他 Linux 指令的模式完全相同。 - 對於需要在背景運行的容器(使用
-d參數),Podman 會啟動一個名為conmon的超輕量級監控程序來作為容器的「監護人」,而podman指令本身則會退出。
因為兩者都完全遵循 OCI 規範,所以用 Docker 建構的映像檔可以在 Podman 上完美運行,反之亦然。它們共享相同的標準,但用截然不同的架構來實現它。
核心差異比較
| 特性 | Docker | Podman |
|---|---|---|
| 核心架構 | 守護行程 (Daemon) 一個中央 dockerd 程序管理所有容器,所有 docker 指令都與它通訊。 | 無守護行程 (Daemonless) 每個指令都是獨立程序,直接建立容器程序,無中央管理者。 |
| 安全性 | root 依賴dockerd 預設以 root 權限運行,成為潛在的單點安全風險。 | 預設無根 (Rootless) 從設計之初就為無根模式優化,可讓普通使用者安全地管理容器,大幅提升安全性。 |
| 系統整合 | 自有管理機制使用 --restart=always 等自有策略管理容器生命週期。 | 與 Systemd 深度整合 可輕易為容器產生 systemd 服務檔,讓容器像標準的系統服務一樣被管理。 |
| 生態系 | All-in-One 平台 Docker 是一個集成了建構、執行、網路等功能的龐大平台。 | 模組化工具集 專注於容器執行,常與 Buildah(建構映像)、Skopeo(操作倉庫)等專業工具搭配。 |
- Docker:如果開發者,
Docker Desktop提供了無與倫比的便利性和整合體驗。其龐大的社群、豐富的文件和成熟的生態系,使其成為入門和快速開發的首選。 - Podman:如果是 SRE/DevOps 或需要在 Linux 伺服器上部署正式環境,Podman 的優勢就非常突出。其無守護行程的輕量架構、以安全為核心的無根模式、以及與
systemd的無縫整合,使其成為一個更穩定、更安全、更符合 Linux 管理哲學的伺服器端容器引擎。
Best Practices
1. 使用 .dockerignore 保持映像檔乾淨
在 Dockerfile 的同層目錄下建立一個 .dockerignore 檔案,語法類似 .gitignore。告訴 Docker 在建置時忽略哪些檔案或目錄。
好處:
- 減小映像檔大小: 避免打包
.git,node_modules,*.log,*.md等不必要的檔案。 - 加速建置: 減少傳輸到 Docker Daemon 的上下文 (build context) 大小。
- 避免快取失效: 修改日誌或 README 不會導致
COPY . .的快取失效。 - 提升安全性: 避免將
.env,id_rsa等敏感檔案複製到映像檔。
2. 提升安全性:使用非 Root 使用者
預設情況下,容器內的程序是以 root 使用者身分執行的,這存在安全風險。最佳實踐是建立一個專用的非 root 使用者來執行應用程式。
FROM python:3.12-slim
WORKDIR /app
# 建立一個非 root 使用者和群組
RUN addgroup --system app && adduser --system --group app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# 切換到新建立的使用者
USER app
CMD ["python", "app.py"]Python 應用程式範例
# 1. 建置階段 (Builder Stage)
FROM python:3.12-slim AS builder
WORKDIR /usr/src/app
# 安裝編譯依賴,並建立虛擬環境
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc && \
python -m venv /opt/venv
# 啟動虛擬環境
ENV PATH="/opt/venv/bin:$PATH"
# 複製並安裝 Python 套件
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 2. 正式環境階段 (Production Stage)
FROM python:3.12-slim
WORKDIR /usr/src/app
# 建立非 root 使用者
RUN addgroup --system app && adduser --system --group app
# 從 builder 階段複製虛擬環境
COPY --from=builder /opt/venv /opt/venv
# 複製應用程式原始碼
COPY . .
# 設定環境變數,讓 python 指向 venv
ENV PATH="/opt/venv/bin:$PATH"
# 切換使用者
USER app
# 設定容器啟動指令
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "my_project.wsgi"]Appendix
什麼是容器化?
[Day2] 淺談 Container 實現原理, 以 Docker 為例(I)
[Day3] 淺談 Container 實現原理, 探討 OCI 實作
Introduction to Containerization: A Beginner’s Walkthrough