TL

OCIランタイムとコンテナ起動シーケンス(runc/containerd)

docker run の裏で何が起きているかを原理から把握。containerd・shim・runc の責務分担と clone() からアプリ起動までの経路を追い、起動失敗やデーモン再起動の挙動を根拠を持って説明できるようになります。

応用コンテナrunccontainerdOCILinux最終更新: 2026-06-21
TL;DR要点だけ先に
  • 1.OCI は2つの仕様に分かれる。Image Spec がイメージ(マニフェスト+レイヤー+config)の形式を、Runtime Spec が展開後のディレクトリ(rootfs+config.json)からコンテナを起動する手順を定める。containerd は前者を後者へ橋渡しする。
  • 2.責務分担は階層的。containerd がイメージ管理とライフサイクルを統括し、コンテナごとに常駐する shim がプロセスの親となって containerd 再起動から切り離し、runc が実際の namespace/cgroup を設定して execve する。runc 自身は起動後すぐ終了する。
  • 3.起動の核心は clone() による namespace 作成と、子プロセスでの pivot_root・cgroup 加入・capability 制限を経た execve。runc は二段階フォークと setns を使い分けてこの分離を実現する。

docker runkubectl run の一行の裏で、イメージのダウンロードからアプリのプロセスが立ち上がるまでには、明確に役割分担された複数のコンポーネントが連携しています。コンテナがカーネルの namespace と cgroup でできていること自体は /devops/container-vs-vm/ で扱いました。本稿はその一段下、OCI 仕様が定める形式と手順に沿って、誰が何を担い、clone() からどう起動するかを追います。

OCI の2つの仕様

OCI(Open Container Initiative)の標準は、混同されがちですが2つに分かれます。

  • Image Spec: イメージの保存形式。manifest(どのレイヤーと config を含むか)、複数の レイヤー(tar+gzip の差分)、config(環境変数・エントリポイント等のメタデータ)で構成され、すべてが コンテンツアドレッサブル(SHA256 ダイジェストで参照)です。
  • Runtime Spec: 「展開済みのコンテナ」をどう起動するかの手順。入力はバンドルと呼ばれるディレクトリで、中身は rootfs/(展開済みファイルシステム)と config.json(namespace・mount・cgroup・capability などの実行設定)の2つだけです。

この2つの間には溝があります。Image Spec のレイヤーは「重ねて使う差分」であり、そのままでは起動できません。レイヤーを順に展開して1枚の rootfs にし、image config から config.json を生成する——この変換を担うのが containerd です。

config.json は実行時に生成される

バンドルの config.json はイメージに固定で入っているものではありません。image config(エントリポイントや環境変数)に、ランタイム側の指定(マウントするボリューム、付与する capability、cgroup の上限など)を合成して、起動のたびに作られます。同じイメージでも docker run の引数次第で異なる config.json になります。

階層的な責務分担

Docker や Kubernetes の標準的なスタックは、上から下へこう並びます。

代表例主な責務
高レベルランタイムcontainerd / CRI-Oイメージの pull・展開、バンドル生成、ライフサイクル統括、CRI/gRPC API の提供
shimcontainerd-shim-runc-v2コンテナごとに常駐し、コンテナプロセスの親になる。STDIO・exit code を中継
低レベルランタイムrunc / crunconfig.json を読み、namespace/cgroup を設定して execve。OCI Runtime Spec の実装本体

なぜ shim という中間層が要るのか。それはライフサイクルの寿命を分離するためです。もし containerd が直接コンテナプロセスの親だと、containerd を再起動・アップグレードしただけで全コンテナが孤児化したり巻き添えで死んだりします。コンテナごとに独立した shim を親に置くことで、containerd が落ちてもコンテナは生き続け、復帰後に shim 経由で状態を再取得できます。shim はまた、コンテナの標準入出力を保持し、終了コードを記録しておく役目も負います。

runc は起動して、すぐ消える

ここが直感に反する重要点です。runc create / runc start を実行すると runc プロセスが動きますが、コンテナの実行中ずっと runc が常駐するわけではありません。runc は namespace と cgroup をセットアップし、コンテナの init プロセスを execve で起動したら、その役目を終えて終了します。

実行中のコンテナの「init プロセス(PID 1)」の親は、runc ではなく shim です。つまり runc は使い捨ての設定係であり、実行中の見張りは shim が担います。この設計のおかげで、何百個コンテナが動いていても runc プロセスが何百個も常駐することはありません。

runc create と runc start の分離

OCI Runtime Spec はコンテナ作成を create(namespace と rootfs を準備し、init プロセスを生成して「一時停止」状態にする)と start(停止していた init プロセスにエントリポイントを実行させる)の2段階に分けています。この分離により、起動直前にネットワーク設定(CNI など)を差し込む隙間が生まれます。Kubernetes のネットワークセットアップはこの間に行われます。

clone() から execve まで

runc が config.json を受け取ってからアプリのプロセスが立ち上がるまでの核心を、原理レベルで追います。本質は 新しい namespace の中に入り、隔離環境を整え終えてから execve するという順序です。

namespace の作成と参加には2系統あります。

  • clone() / unshare(): CLONE_NEWPIDCLONE_NEWNS などのフラグを与え、新しい namespace を作って子プロセスをその中で生成する。
  • setns(): すでに存在する namespace(例: Pod 内で共有するネットワーク namespace)に既存プロセスを参加させる

runc は内部で C コードの段階(nsexec)を二段階フォークで通過します。PID namespace は特殊で、clone() を呼んだプロセス自身は新 PID namespace に入らず、その子が PID 1 になるため、正しく「コンテナ内 PID 1」を作るには追加のフォークが必要だからです。概念的な順序は次の通りです。

1. config.json を解析(namespace・mount・cgroup・capability の一覧を取得)
2. clone(CLONE_NEWPID|CLONE_NEWNS|CLONE_NEWNET|CLONE_NEWUTS|CLONE_NEWIPC ...)
   → 新しい namespace 群の中に子プロセスを生成
3. (子の中で)cgroup に自プロセスを加入させ、CPU/メモリ上限を適用
4. rootfs を準備:bind mount を張り、pivot_root で rootfs を新しい「/」に切替
5. /proc, /sys, /dev など必要な擬似ファイルシステムをマウント
6. capability を絞り込み、no_new_privs をセット、seccomp フィルタを適用
7. setuid/setgid でコンテナ内ユーザーに降格
8. execve(エントリポイント) → ここで runc のコードは消え、アプリ本体に置き換わる

ステップ4の pivot_root が「コンテナからホストのファイルシステムが見えない」理由の中核です。mount namespace の中でルートを rootfs に挿げ替え、元のルートをアンマウントすることで、コンテナはホストの / へ遡れなくなります(chroot より堅牢で、抜け出しにくい)。

設定の順序が安全性を決める

capability の剥奪や seccomp の適用が execve の前に完了している点が重要です。もし権限制限がアプリ起動後だと、ごく短時間でも全権限で任意コードが走る窓が開きます。OCI ランタイムは「制限を全部かけ終えてから初めてアプリを exec する」順序を厳守することで、この窓をなくしています。no_new_privs は、以降 setuid バイナリなどで権限を再取得できないようにするカーネルフラグです。

shim を介した状態管理と終了

execve でアプリ(init プロセス)が走り出すと、その親は shim です。shim はコンテナの終了を waitpid で待ち受け、終了コードを保持します。docker ps で停止済みコンテナの exit code が見えるのは、shim(が containerd に渡した情報)のおかげです。

Kubernetes ではこの低レベルランタイムを kubelet が直接叩くのではなく、CRI(Container Runtime Interface)という gRPC API を介して containerd / CRI-O に依頼します。kubelet は「この Pod を起動せよ」と CRI で要求し、containerd が Pod 内コンテナぶんのバンドルを作って runc を呼ぶ——という分業です。この標準化のおかげで、ランタイムを差し替えても上位の Kubernetes 側は無改変で動きます。

まとめ:層を貫く一本の道

整理すると、docker run から始まる一連は次のように流れます。

  1. containerd: イメージを pull し、レイヤーを展開して rootfs を作り、image config と実行指定を合成して config.json を生成。バンドルが完成。
  2. shim: コンテナごとに起動し、runc を呼び出す親プロセスとなる。containerd の寿命からコンテナを切り離す。
  3. runc: config.json を解釈し、clone() で namespace を作り、cgroup 加入・pivot_root・capability 制限・seccomp を順に適用してから execve。設定が済むと runc は退場。
  4. アプリ: コンテナ内 PID 1 として走り、親の shim が終了を見届ける。

この分業は単なる実装都合ではなく、寿命の分離(shim)・標準化(OCI/CRI)・最小権限の確実な適用(runc の順序) という設計目標の現れです。なぜ containerd を再起動してもコンテナが死なないのか、なぜランタイムを crun に差し替えられるのか、なぜ起動失敗が「pull」「mount」「exec」のどの段で起きたか切り分けられるのか——その根拠はすべてこの階層構造にあります。コンテナの不変性を前提にしたデプロイ思想(/devops/immutable-infra/)や、イメージを成果物として扱う CI/CD(/devops/ci-cd/)も、この再現可能な起動経路の上に成り立っています。

DevOps/インフラ Article

OCIランタイムとコンテナ起動シーケンス(runc/containerd)を実務で読む

TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。

解決すること

コンテナ

比較で見る軸

難易度: advanced / カテゴリ: DevOps/インフラ / タグ数: 5

導入後に効く点

責務分担は階層的。containerd がイメージ管理とライフサイクルを統括し、コンテナごとに常駐する shim がプロセスの親となって containerd 再起動から切り離し、runc が実際の namespace/cgroup を設定して execve する。runc 自身は起動後すぐ終了する。

先に潰すリスク

用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。

数字・仕様の読み方
難易度
advanced
カテゴリ
DevOps/インフラ
タグ数
5

判断チェックリスト

  • 自社の用途が「コンテナ / runc」に近いか確認する。
  • 強みである「OCI は2つの仕様に分かれる。Image Spec がイメージ(マニフェスト+レイヤー+config)の形式を、Runtime Spec が展開後のディレクトリ(rootfs+config.json)からコンテナを起動する手順を定める。containerd は前者を後者へ橋渡しする。」が本当に評価軸になるか確認する。
  • 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
  • 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
  • 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
  • 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。

次に確認する観点

コンテナrunccontainerdOCILinuxコンテナrunccontainerd