OCI 入門:容器世界的共同語言

多數工程師認識容器的第一步都是從 Docker 開始,因此在日常溝通、內部文件甚至技術面試中,「Docker image」幾乎成了容器映像檔的代名詞。然而在標準文件中,還會看到另一個名稱:OCI image。兩者並非完全相同的格式,但核心結構高度相容,Docker 也能建置與執行 OCI image。

這兩個名詞的淵源始於 2017 年。當時 Open Container Initiative(開放容器倡議,簡稱 OCI)發布 Image Spec v1.0,Docker 工程師 Stephen Day 指出:

「很高興看到 Docker 的映像格式被納入 OCI 映像規格。」

這項轉變意味著 Docker 將自家的格式捐贈給社群,確立為全產業通用的開放標準。

這就像「立可白」原本是特定商標,後來成了修正液的通稱;差別在於,容器世界習慣直呼品牌名,真正的底層標準名稱反而較少被提起。理解兩套格式的相容關係後,就不會再誤以為「Docker 建置的映像檔只能在 Docker 上執行」。


為什麼容器需要一套開放標準

如果把現代容器比喻為遠洋海運的貨櫃,標準的重要性便十分清楚。貨櫃的長寬高、鋼角構件與防風鎖扣都具備嚴格的國際標準,因此無論抵達哪座港口,吊車都能精準抓取;不論是哪家船運公司的貨輪,都能緊密堆疊。

貨物靠岸後,任何符合規範的聯結車亦能直接載運。港口、船隊與陸運業者無需事先逐一協商,也不必過問貨櫃內部承載的是紡織品還是精密晶片。

軟體容器也是相同的邏輯。若缺乏統一標準,每個工具都會發展出專屬的打包格式,用 A 工具建立的服務便無法在 B 工具中啟動,開發者將陷入嚴重的廠商綁定(vendor lock-in)。在 OCI v1.0 發布之際,SUSE 的 Aleksa Sarai 與 Red Hat 的 Mrunal Patel 分別指出了標準的核心價值:

「有了標準,使用者可以自由組合不同元件,不必擔心被廠商綁定。」—— Aleksa Sarai,SUSE

「標準化能確保容器裡的應用程式可以在不同 runtime 之間移植。」—— Mrunal Patel,Red Hat

一套公認的標準為工程團隊帶來極為具體的實務價值:

標準本身雖不直接提升單一工具的運算速度,但它讓生態系得以高度分工:讓建置、執行與儲存各司其職,再透過一致的格式與介面相互銜接。


OCI 的誕生與治理架構

2015 年 6 月,Docker、CoreOS 與各大雲端廠商在 Linux 基金會支持下共同創立 OCI,致力於制定容器格式與執行階段(runtime)的開放標準。Docker 當時捐出自家的容器格式與底層執行工具 runc,作為這套開放架構的核心基石。

在組織層面上,OCI 是運作於 Linux 基金會底下的中立開放治理架構。它不隸屬於任何單一雲端巨頭或商業公司,所有規格演進皆須透過多方公開討論與審查。

標準的演進速度通常落後於商業產品的創新步伐。從組織成立到規格初版問世歷時兩年,而負責傳輸與倉庫 API 的 Distribution Spec(發布規格)更耗時將近六年才正式定稿。

但這種嚴謹的節奏反而是架構穩定性的保障:凡是被寫入 OCI 規格的設計,皆已歷經全球生產環境的嚴苛驗證,具備長期的向後相容性。


三大核心規格各司其職

OCI 目前主要由三份相互銜接的核心規範所構成:

規格名稱規範範疇核心職責與比喻
Image Spec映像檔的儲存格式與結構定義貨櫃的尺寸規格與貨單清單
Runtime Spec容器的生命週期與執行環境定義貨櫃送達後的拆封、通電與啟動規範
Distribution Spec映像檔的網路傳輸與倉庫 API定義港口裝卸、貨物登記與提領流程

Image Spec(映像檔規格)規範了容器映像檔的組裝架構,包含 Manifest(清單)、選填的 Image Index(多架構索引)、分層檔案系統(Layers)以及執行設定檔(Config)。其宗旨是確保不同工具產出的映像檔,都能被相容的工具正確解讀與拆封。

Runtime Spec(執行階段規格)規範了容器如何被實際建立與運作,包括設定檔 config.json 的定義、Linux 命名空間與 cgroups 的隔離環境,以及 create、start、kill、delete 等生命週期控制指令,確保同一個容器在不同 runtime 底下的行為完全一致。

Distribution Spec(發布規格)則以 Docker Registry HTTP API V2 為基礎,定義了映像倉庫(Registry)必須實作的標準 RESTful API,例如透過 GET /v2/<name>/manifests/<tag|digest> 查詢並拉取清單。

這三份規格刻意留白了一件事:它們完全未限制映像檔該如何建置。Dockerfile 本身並不是 OCI 標準的一部分。正因如此,社群才得以誕生出 BuildKit、Buildah 等百花齊放的建置引擎,只要最終產出符合 Image Spec,就能無縫融入整個容器生態。


映像檔的內部結構與分層機制

許多初學者直覺以為容器映像檔是一個巨大的單一二進位檔案,但它實際上是由一份 JSON 格式的 Manifest,搭配數個壓縮打包的檔案系統層所組成。

一份精簡後的 OCI Manifest 結構如下:

{
  "schemaVersion": 2,
  "mediaType": "application/vnd.oci.image.manifest.v1+json",
  "config": {
    "mediaType": "application/vnd.oci.image.config.v1+json",
    "digest": "sha256:b5b2b2c5...",
    "size": 7023
  },
  "layers": [
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:9834876d...",
      "size": 32654
    }
  ]
}

理解映像檔結構只需要掌握四個關鍵欄位:

  1. mediaType:宣告該檔案的 MIME 格式,讓工具能辨識這是 OCI Manifest 還是設定檔。
  2. config:指向記錄執行細節的 JSON 物件,包含環境變數、預設工作目錄、啟動指令(Entrypoint/Cmd)等中繼資料。
  3. layers:一組按順序堆疊的壓縮檔(tar+gzip),依序解開並覆蓋後即構成容器看到的完整根目錄檔案系統。
  4. digest:內容的密碼學雜湊值(通常為 SHA-256),作為該檔案的全球唯一指紋。

Manifest、Blob 與 Tag 如何串起映像檔

Manifest 本身就像一張「提貨單」,並不直接夾帶檔案內容,而是透過 Digest 指向真正的資料區塊。客戶端工具取得 Manifest 後,再依照 Digest 分別下載 Config 與對應的 Layer。

反過來看,推送映像檔也不是上傳一個巨大的單一檔案。執行 docker push 時,Docker 會先依照 Digest 檢查 Registry 已有哪些 Blob,只上傳缺少的 Layer 與 Config,最後才上傳描述這些物件關係的 Manifest。

因此,Registry 並非以單一封裝檔保存映像檔,而是分別儲存 Manifest、Config 與多個 Layer。這些物件共同構成映像檔這個邏輯集合;myapp:v1 這類 Tag 最終指向的也是 Manifest。

同一個 Layer Blob 也能被多份 Manifest 共同引用。只要 Digest 完全一致,Registry 與客戶端就無需重複儲存或傳輸該 Layer;反之,即使兩層包含的檔案看似相同,只要內容或中繼資料有任何差異,就會產生不同的 Digest,並被視為不同的 Layer。

 +------------------------------------------------------+
 | OCI Manifest                                         |
 |                                                      |
 | config -> sha256:b5b2b2c5...                         |
 | layers -> [ sha256:9834876d..., sha256:4a3f12c8... ] |
 +------------------------------------------------------+
             |                             |
             v                             v
 +------------------------+    +------------------------+
 | Image Config           |    | Image Layers (tar.gz)  |
 |                        |    |                        |
 | - ENV                  |    | - Layer 1 (OS Base)    |
 | - WORKDIR              |    | - Layer 2 (Packages)   |
 | - CMD                  |    |                        |
 +------------------------+    +------------------------+

Layer 的共用與快取原理

容器的分層機制仰賴兩項核心技術:內容定址儲存(Content-Addressable Storage)與聯合檔案系統(Union Filesystem,Linux 實務上主流為 OverlayFS)。每一層 Layer 僅記錄相較於前一層的新增、修改或刪除差異。

映像檔中的 Layer 都是唯讀內容。容器啟動時,Runtime 會在這些 Layer 上方增加一個可寫層;執行期間新增或修改的檔案只會寫入這一層,不會改動原始映像檔。這個可寫層不屬於 OCI image,也不會隨 docker push 上傳。

以一個標準的 Python 應用為例,映像可能由五層構成:基礎作業系統、Python 執行環境、套件清單、相依套件與應用程式原始碼。當同一台主機部署另一個 Python 服務時,前四層的 Digest 完全一致,本機可以直接共用既有檔案,僅需額外下載並儲存第五層的原始碼。

這項機制直接解釋了日常開發中的多個常見現象:


Tag 與 Digest:為什麼生產環境不可使用 latest

在拉取映像檔時,主要有兩種指定標識的方式:Tag 與 Digest。在 Distribution Spec 中,兩者有著根本性的語意差異:

簡而言之,「Tag 像是隨時可撕下的便利貼,Digest 則是無法偽造的指紋」。latest 僅是一個慣用的預設 Tag 名稱,並不代表「最新且最穩定」的版本,任何人在任何時間推送同名標籤,latest 的指向就會隨之更動。

Kubernetes 官方文件強烈建議在生產環境中避免使用 :latest,否則將導致版本追蹤困難且難以精準還原。團隊應採用明確的版本 Tag(如 v1.42.0),或在部署設定中直接鎖定 Digest:

# 推薦作法:使用具體版本標籤
image: myapp:v1.42.0

# 最佳實踐:直接綁定 Digest,保證內容絕對一致
image: myapp@sha256:45b23dee08af8d1a3c7b8e1f0e2d4c6b8a0...

WARNING

不同節點可能各自執行不同版本:若在不同時間點有多個節點分別拉取 myapp:latest,期間又有新映像推送到倉庫,各節點將執行截然不同的程式碼版本,部署檔案上卻完全看不出差異。採用 Digest 能徹底消除這種不確定性。

在實務上,工程師之間溝通時常使用版本 Tag 以利辨識,而在自動化部署腳本中則寫入 Digest 以確保生產環境的穩定與可重現性。


多架構映像:跨 CPU 平台的無縫相容

許多人發現官方的 alpine 映像檔,無論在 x86 架構的筆電、ARM 架構的 Apple Silicon Mac 還是樹莓派上都能正常執行。這並非因為映像檔能動態轉譯機器碼,而是因為該 Tag 背後封裝了 Image Index(亦稱 Manifest List)。

Image Index 是一份「清單的清單」,裡面列出了多組平台架構(例如 linux/amd64 與 linux/arm64)及其對應的個別 Manifest Digest。

當客戶端發起拉取請求時,Registry 會先回傳這份 Index,容器工具再依據目前主機的 CPU 架構自動挑選相容的 Manifest 進行下載。整個過程由工具在底層自動協調,使用者只需輸入相同的指令即可。

 +------------------------------------------------------+
 | Image Index (Manifest List)                          |
 |                                                      |
 |   Platform: linux/amd64  -->  Manifest (Digest A)    |
 |   Platform: linux/arm64  -->  Manifest (Digest B)    |
 +------------------------------------------------------+
              |                             |
              v                             v
 +------------------------+    +------------------------+
 | Manifest (amd64)       |    | Manifest (arm64)       |
 |                        |    |                        |
 |   Config: amd64 bin    |    |   Config: arm64 bin    |
 |   Layers: amd64 rootfs |    |   Layers: arm64 rootfs |
 +------------------------+    +------------------------+

反之,若映像檔在打包時僅針對單一架構編譯,一旦在不相容的平台上啟動,作業系統便會拋出 exec format error 的核心錯誤。


執行流程接力:從 docker run 到容器啟動

高階與低階 Runtime 的分工

「高階」與「低階」並非 OCI 規格的正式分類,而是容器生態系常用的架構稱呼,指的是抽象層次,而非效能或品質。高階 Runtime 負責決定要執行什麼並準備所需資源;低階 Runtime 則接手建立隔離環境,真正啟動容器行程。

 Docker / Kubernetes
         |
         v
 containerd / CRI-O
 (高階 Runtime:管理映像檔與容器生命週期)
         |
         v
 runc / crun / runsc
 (低階 Runtime:建立隔離環境並啟動行程)
         |
         v
 Linux 核心與容器行程

三份規格的接力流程

當我們在終端機敲下 docker run alpine 時,三份 OCI 規格會在幕後依序銜接,由高階與低階 Runtime 分工完成流程交接:

 1. Distribution Spec (下載)
    Registry ---> [Image Index / Manifest]
       ---> [Config + Layers]

 2. Image Spec (解讀)
    解壓縮各層 Layers ---> 組合成完整 Root Filesystem

 3. 格式轉換 (Bundle 生成)
    Image Config + Rootfs
       ---> Filesystem Bundle (config.json + rootfs/)

 4. Runtime Spec (執行)
    runc / crun 讀取 bundle
       ---> 建立 Namespaces / Cgroups
       ---> 啟動行程

具體接力步驟如下:

  1. Distribution Spec 接棒(下載):高階 Runtime(如 containerd 或 CRI-O)向 Registry 發出查詢請求。若回傳為 Image Index,則依據主機 CPU 挑出對應的 Manifest,接著下載 Config 與尚未快取的各層 Layers。
  2. Image Spec 接棒(解讀):高階 Runtime 依據 Manifest 依序解壓縮各層 Layer,並透過 OverlayFS 疊合成容器的根檔案系統。
  3. 產生 Filesystem Bundle:高階 Runtime 依據 Image Spec 將映像轉換為 Runtime Spec 所需的 Filesystem Bundle 目錄。此目錄包含解開後的 rootfs/ 資料夾,以及由 Image Config 轉譯而來的 config.json(包含指令陣列、環境變數與工作路徑)。
  4. Runtime Spec 接棒(執行):低階 Runtime(如 runc)接手讀取該 Bundle 目錄,透過 Linux 核心系統呼叫建立 Namespaces、Cgroups 與 Seccomp 安全沙盒,最終啟動容器主行程。

這種設計揭示了一個核心觀念:Dockerfile 裡的各項指令並非特定廠商的私有魔法,而是被標準化地封裝於 OCI Config 中,再經由通用的轉譯規則交付給底層 Runtime 執行。


容器生態系的分層架構與零件置換

OCI 標準確立後,容器技術堆疊被清晰地劃分為多個獨立層級,每一層皆具備高度的可替代性:

 +--------------------------------------------------------+
 | User Tools                                             |
 | Docker CLI, Podman, kubectl                            |
 +--------------------------------------------------------+
                |
                v
 +--------------------------------------------------------+
 | High-Level Runtimes                                    |
 | containerd, CRI-O                                      |
 +--------------------------------------------------------+
              |                              |
              v                              v
 +-------------------------+    +-------------------------+
 | Low-Level OCI Runtime   |    | OCI Image Registry      |
 |                         |    |                         |
 | runc (standard default) |    | Docker Hub, GHCR,       |
 | crun (fast C impl.)     |    | Harbor, ECR             |
 | gVisor runsc (strong    |    |                         |
 | isolation)              |    |                         |
 +-------------------------+    +-------------------------+

各層級的代表性實作與置換彈性如下:

在高度重視安全防護的多租戶環境中,維運團隊可直接將預設的 runc 置換為 gVisor 的 runsc,上層的 Kubernetes 部署設定與應用程式映像檔完全無需修改,即可獲得強化的核心隔離保護。


Registry 的延伸:從映像倉庫到通用成品庫

隨著 Distribution Spec 的成熟,社群發現其以 Digest 定址與儲存內容的架構極具擴充性,完全能勝任容器映像檔之外的資料分發需求。

Kubernetes 著名的套件管理工具 Helm 自 v3.8.0 起,正式原生支援將 Helm Chart 以 OCI 格式打包並推送到標準 Registry 中:

# 將 Chart 打包並推送到 OCI Registry
helm push mychart-0.1.0.tgz oci://localhost:5000/helm-charts

# 直接從 OCI Registry 安裝 Chart
helm install myrelease oci://localhost:5000/helm-charts/mychart --version 0.1.0

2024 年發布的 OCI v1.1 規格更進一步擴充了 Referrers API 與 Artifact 支援,允許將數位簽章(Cosign 等防偽驗證資料)、SBOM(軟體物料清單)甚至 AI 模型檔案以中繼資料形式「關聯」至特定的映像檔 Digest 上,隨同映像一併存放於 Registry。

OCI 在規格演進時亦充分考慮向後相容性,未升級的舊版 Registry 會安全地忽略未識別的新欄位,讓整個雲原生生態得以平滑升級。這使得企業只需維護單一 Registry,即可統整容器映像檔、Helm 套件與資安合規清單的權限控制與稽核軌跡。


本機實證:親眼檢視 OCI Manifest

要具體感受 OCI 規格的真實運作,最直接的方法是透過 Docker CLI 檢視遠端 Registry 上的原始 Manifest 資料:

# 檢視 alpine 映像檔在 Registry 上的平台清單(Image Index)
docker buildx imagetools inspect alpine

# 輸出底層原始 JSON 格式資料
docker buildx imagetools inspect --raw alpine

執行上述原始檢視指令時,回傳的 JSON 片段會直接印證前面章節介紹的結構:

{
  "schemaVersion": 2,
  "mediaType": "application/vnd.oci.image.index.v1+json",
  "manifests": [
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "digest": "sha256:c0537ff6101e8b40b64431842d9b92161d5...",
      "platform": {
        "architecture": "amd64",
        "os": "linux"
      }
    },
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "digest": "sha256:39f60e94200dbf3e58dd2f2930263f1ecf3...",
      "platform": {
        "architecture": "arm64",
        "os": "linux"
      }
    }
  ]
}

從中可以清楚看到頂層的 mediaType 宣告為 Image Index,並依據 platform 列出不同 CPU 架構所對應的個別 Manifest Digest。


結語與延伸學習

理解 OCI 不只是釐清術語由來。當看清「Image 定義結構、Distribution 負責傳輸、Runtime 驅動執行」的分工後,未來在面對跨平台編譯、安全執行階段抽換或部署版本鎖定等問題時,便能更準確地定位問題發生在哪一個層級。

若想進一步深入各項規格的實作細節,可參考下列官方文件與規格原始碼: