Dockerコンテナの10年
(cacm.acm.org)- 2013年の初公開以来、開発者のアプリケーションのビルド・配布・実行方法を根本から変えてきた Docker の技術的進化の過程に光を当てる ACM 論文であり、単純な CLI の背後にある数十年にわたるシステム研究を整理している
- Linux の名前空間を活用し、仮想マシンなしでもプロセス分離を実現する方式が Docker の中核的な技術基盤であり、2001年から段階的に追加された 7 種類の名前空間を組み合わせて軽量コンテナを実装
- macOS と Windows のサポートのため、ライブラリ仮想マシンモニタ(HyperKit) をデスクトップアプリ内部に組み込む逆転の発想のアーキテクチャを採用し、従来のハイパーバイザ方式の代わりにユーザープロセス内で Linux を実行
- 現在は ARM・RISC-V などの異種ハードウェアと AI ワークロードをサポートし、クラウド・デスクトップ・エッジ全般で標準的な開発インフラとして定着
- AI ワークロードの台頭によりGPU 依存関係管理が新たな課題として浮上しており、CDI(Container Device Interface)による GPU サポートや TEE(信頼実行環境)統合など、Docker の進化は現在も続いている
技術的起源
- 2000年代初頭には、Linux ディストリビューションに多数の依存関係を手動でインストールし、ソフトウェアを直接コンパイル・設定するのが一般的であり、2010年にクラウドコンピューティングが台頭するとこの過程はいっそう複雑になった
- Docker は、開発者がアプリケーションとそのすべての依存関係を**ファイルシステムイメージ(「コンテナ」)**としてパッケージ化し、Docker だけがインストールされたどのマシンでも実行できるように単純化した
- 仮想マシンと違い、完全な OS をインストールせず、いくつかのコマンドだけで実行可能
一般的なワークフロー
- 開発者が Dockerfile を作成すると、シェル構文ベースの段階的なビルド過程を定義する
- Python Web サイトの例:
FROM python:3で始め、依存関係のインストール、コードのコピー、ポートの公開、実行コマンドまでを 1 つのファイルで記述
- Python Web サイトの例:
docker buildでコンテナイメージを生成し、docker pushで Docker Hub にプッシュdocker run -v data:/app/data -p 80:80のように、データボリュームのマウントとネットワークポートの公開を指定して実行- 2013年以降、CLI は大きく拡張され、バックエンドは全面的に再設計されたが、Dockerfile 作成 →
docker build→docker runという基本ワークフローは一貫して維持されている - GitHub では、公開リポジトリのルートに置かれた Dockerfile が340万個以上見つかっている
内部動作の仕組み: Linux 名前空間
- OS カーネルはプロセスメモリを分離するが、ファイルシステム、設定ファイル、動的ライブラリなど多くのシステムリソースは共有状態にある
- 同一マシンに競合する動的ライブラリ要件を持つ複数のアプリをインストールするのは非常に難しい
- ネットワークポートの競合など、プロセス間で望ましくない干渉が発生しうる
- 各アプリを個別の**仮想マシン(VM)**で実行すれば解決できるが、カーネル・ファイルシステム・キャッシュ・ブリッジネットワークの重複により非常に重い
- 各ゲスト OS が独立して動作するため、ストレージ・メモリの重複排除も難しい
- 1978年の Unix v7 の
chroot()は別個のルートファイルシステムを可能にしたが、複数アプリのファイルシステム合成はサポートしていなかった - Nix と Guix は、アプリごとのディレクトリ再パッケージ化 + 動的リンクでこれを解決するが、プロプライエタリソフトウェアには適用しにくく、ネットワークポート競合の問題も解決できない
- Docker は Linux の名前空間を選択した: 各プロセスがファイル・ディレクトリなど共有リソースへのアクセス方法を個別に制御できる
- 例: 異なる名前空間にある 2 つのプロセスが
/etc/passwdをそれぞれ/alice/etc/passwdと/bob/etc/passwdとして別々に解釈 - リソースを開くときだけ名前空間が適用され、その後ファイルディスクリプタは追加オーバーヘッドなしに通常のカーネルリソースとして動作
- 例: 異なる名前空間にある 2 つのプロセスが
- 名前空間導入の沿革
- 2001年 Linux 2.5.2: マウント名前空間
- 2006年 Linux 2.6.19: IPC 名前空間
- 2007年 Linux 2.6.24: ネットワークスタック名前空間
- 合計 7 種類の名前空間をサポート
- Plan 9 と異なり、名前空間は最初から設計されたのではなく段階的に追加されたため、低水準で使いにくかった
- FreeBSD や Solaris の類似機能も一般的な利用には至らなかった
- 2013年の Docker の核心的貢献: VM の重い分離とOS 基本要素の使いやすさの間にある実用的な均衡点を見いだしたこと
Docker の Linux コンテナ実行構造
- Docker はクライアント-サーバー構造で、ホスト上で動作するサーバーデーモン(
dockerd)と、RESTful API を通じて要求を送る CLI クライアントで構成される - 2015年ごろ、モノリシックなデーモンを分離して専門化されたコンポーネントへ再構成
- BuildKit: ファイルシステムイメージを組み立てる
- containerd: イメージを実行中コンテナとしてインスタンス化し、ネットワーク・ストレージリソースを管理
コンテナイメージ
docker buildを呼び出すと、Dockerfile の実行ファイルとデータを表すレイヤー化ファイルシステムイメージを生成- 最下位レイヤー: Debian や Alpine Linux などの OS ディストリビューション(または tar アーカイブとして手動で構築)
- その後のレイヤー: Dockerfile の個々のコマンド実行結果として生じたファイルシステム差分
- コンテンツアドレス指定ストレージシステムに保存: ファイルシステムイメージのハッシュをキーとして管理
- 効率的な重複排除、不変性の保証、ハッシュによる改ざん検証
- 2016年、Open Container Initiative(OCI) でイメージフォーマットを標準化し、多数の独立実装が存在
- Linux ファイルシステムの overlayfs、btrfs、ZFS を活用して copy-on-write レイヤーを効率的にスナップショット・クローン
stargzストレージスナップショッタによるイメージの**遅延プル(lazy-pulling)**をサポート
コンテナインスタンス
docker runを呼び出すと、OCI イメージから名前空間で分離されたプロセス(「コンテナ」)を生成するためにシステムリソースを割り当てるcontainerdが各コンテナに必要な名前空間を動的に設定しながら実行する処理:- リソース分離と I/O 速度制限のためのプロセス**cgroups(制御グループ)**を定義
- コンテナ内部のローカルネットワークポートをホストインターフェース上の外部公開ポートへ再マッピング
- 永続的なアプリ状態のためにホストファイルシステムの可変ストレージボリュームを接続
- PID 名前空間でコンテナのプロセスツリーを分離
- ユーザー名前空間でコンテナ内のローカル UID をホスト上の別の UID にマッピング(例: コンテナ内 UID 1000 → ホスト上では UID 12345 または 23456)
- 名前空間の構成には多少のオーバーヘッドがあるが、完全な Linux VM の起動よりはるかに小さく、大半は 1 秒未満
- Linux カーネルは終了したコンテナを通常のプロセスのようにガベージコレクションする
Linux を越えて: macOS と Windows のサポート
- クライアント-サーバーアーキテクチャのおかげで、CLI はセキュアなネットワーク接続を通じてリモートの Docker インスタンスにコマンドを送信できる
- 2015年、Docker は Linux 開発で広く採用されていたが、macOS/Windows 開発者が Linux コンテナを実行できないというユーザビリティの壁に直面
- 大半の開発者は macOS/Windows を基本開発環境として使っているが、Docker のファイルシステムイメージは Linux カーネル上でしか実行できない
- パブリッククラウドの台頭により、デプロイ環境としては Linux が好まれていた
Docker for Mac アプリケーションの構築
- 中核的な制約: Linux版Dockerに慣れた開発者に対して、追加設定なしで動作し、同一のDockerイメージを実行できなければならない
- 従来方式(LinuxをデスクトップOSの横で別個に実行)を逆転し、ハイパーバイザーをmacOS/Windowsのユーザー空間アプリ内部に組み込み、その中でLinuxを実行
- **ユニカーネル(unikernel)**研究から着想: OSコンポーネントをより大きなアプリケーション内に柔軟に組み込めることを実証
- HyperKit: Intel CPUのハードウェア仮想化拡張を使って、通常のユーザープロセスでLinuxカーネルを実行するライブラリVMM
- 組み込みLinuxカーネルがDockerデーモンを実行し、これがコンテナを管理して通常のDockerサーバーエンドポイントとして機能
- Linux管理のあらゆる詳細はデスクトップアプリ内部に隠蔽 → デスクトップの
docker buildとdocker runは組み込みLinuxインスタンスに転送され、「そのまま動く」
- このアプローチはPodmanなど他のコンテナシステムにも採用され、macOS/Windowsでコンテナを実行する標準的な方法として定着
LinuxKit: 組み込み向けカスタムLinuxディストリビューション
- 単独実行ではなく、他のアプリのコンポーネントとして組み込まれるよう設計されたカスタムLinuxディストリビューション
- アプリ起動時間を最小化するため、Dockerコンテナ実行に必要な最小限のコンポーネントだけを含むカスタムユーザー空間を構築
- すべての単一コンポーネントをコンテナ内部で実行し、起動時に使われるルート名前空間では何も実行しない
- Dockerコンテナが使うものと同じcopy-on-writeファイルシステムとネットワーク名前空間を活用
- LinuxKit + HyperKitの組み合わせにより、ネイティブmacOSプロセスとほぼ同等の速度でLinuxプロセスを起動可能
- 2016年にDocker for MacおよびWindowsアプリとしてリリース
ネットワーキング問題とSLIRPの解決策
- 組み込みLinuxコンテナからmacOS/Windowsへのネットワーク接続は予想外に厄介な問題
- 従来のEthernetブリッジ方式は複雑なネットワーク管理を必要とし、企業デスクトップのファイアウォール・ウイルスチェッカーが潜在的な悪意あるトラフィックとして検知したため、ベータユーザーから数千件のバグレポートが発生
- SLIRPによる解決策: 1990年代半ばにPalm Pilot PDAをインターネットへ接続するために初めて使われたツール
- コンテナのTCPハンドシェイク時に、Ethernetフレームが共有メモリを通じてvirtioプロトコルでホストへ送信
- MirageOSのユニカーネルライブラリを活用し、Linuxのネットワーキング要求をmacOS/Windowsネイティブのソケット呼び出しへ変換
- OCamlで書かれたユーザー空間TCP/IPスタックvpnkitがホストOSで受信し、macOSの
connect()システムコールを呼び出す - VPNポリシーの観点では、外向きトラフィックは別マシンではなくDockerアプリから発生したものとして認識
- 2016年のベータテストでvpnkitを展開後、企業ユーザーのバグレポートは99%以上減少
- SLIRPアプローチはその後サーバーレスクラウド分野でも採用され、古いダイヤルアップネットワーキング技術が新しいコンテナ管理の問題解決に活用された
インバウンドネットワークトラフィックの処理
- Linuxコンテナがポートをlistenしていても、CLIで明示的に要求しない限り自動的にインターネットへ公開されない(例:
docker run -p 80:80 nginx) - 理想的なユーザー体験: コンテナポートがデスクトップIPに直接現れ、
http://localhost:8080でブラウザーからアクセス可能- VMware Fusionのような既存のデスクトップ仮想化では、
localhostの代わりに一時的な中間IPを公開
- VMware Fusionのような既存のデスクトップ仮想化では、
- LinuxKitカーネルにカスタムeBPFプログラムをインストール → デスクトップホストに対応するlistenソケットの生成をトリガー → ポートフォワーダーを有効化
- 結果: MacでLinuxコンテナを実行すると即座に
localhostでアクセス可能となり、ネイティブLinuxマシンと同じ開発者体験を実現
ストレージ
- 開発者はコードをローカルで編集しながら、コンテナ内でコードとテストを実行する必要がある
- Linuxでは
docker run -v /host:/containerでバインドマウントによりライブなファイルアクセスが可能 - macOSとWindowsは異なるカーネルのため、バインドマウントは動作しない
- DockerはKVMハイパーバイザーに由来するvirtio-fs共有メモリプロトコルを使い、ファイルシステム操作をホストへ転送(FUSEリクエスト形式)
- ホストがこのリクエストを受信し、対応する
open、read、writeシステムコールを呼び出す
- 開発者のコードとデータはホストファイルシステムに保持されるため、AppleのTime CapsuleやSpotlightのようなバックアップ・検索ツールからアクセス可能
Windows Services for Linux (WSL)
- 2017年にMicrosoftがWSLをリリース: Windows上で直接Linuxアプリを実行
- 最初のバージョンは仮想化ではなく、LinuxバイナリのシステムコールをWindowsシステムコールへ動的変換するライブラリOS方式
- 多くのアプリでは成功したが、Dockerコンテナ実行には対応するシステムコールが不足
- 2018年にWSL2をリリース: Docker for Macと同様、バックグラウンドで完全なLinux VMを実行する方式へ再設計
- WSL2 DockerはLinuxKit WSLディストリビューション内でデーモンとユーザーコンテナを実行
- Windows本体および他のLinuxディストリビューションからDocker APIとネットワークのポートフォワーディングを処理
- Dockerコンテナのクロスプラットフォーム進化を可能にした中核アーキテクチャ: 伝統的に「カーネル専用コード」だったものをユーザー空間ライブラリとして再利用し、他アプリに組み込むライブラリOSアプローチ
- このアーキテクチャの成功は、見えないまま遍在していることによって証明されている: 何百万人もの開発者が毎日、どのOS上で動いているかを気にせずDockerを使っている
新しい開発者ワークフロー: 複数CPUアーキテクチャ
- Docker初期には、クラウドワークロードの大半がIntelアーキテクチャベースだった
- 2018年のAmazon Graviton ARMプロセッサ、2020年のApple M1 ARM CPUの登場で状況が変化
- ARMでワークロードを実行するとコスト削減と性能向上が可能
- 同一Dockerイメージ内で、Intel、ARM、POWER、RISC-Vなど複数CPUアーキテクチャへの対応が必要
- サーバー側では、OCIイメージフォーマットを**マルチアーキテクチャマニフェスト(multiarch manifests)**へ拡張
- 単一ホストで複数CPUアーキテクチャ向けイメージをビルドするため、Linuxの
binfmt_misc機能を活用- QEMUによりARMとIntelバイナリ間を透過的に変換
- 主にビルド段階でのみオーバーヘッドが発生し、結果として得られるマルチアーキテクチャイメージはどのホストでもネイティブ実行
- AppleがRosettaを通じてCPU命令セット変換のハードウェア・ソフトウェア支援を導入 → Dockerアーキテクチャへ容易に統合
- 現在ではIntelコンテナとARMコンテナを並行して実行することが一般的な開発者ワークフロー
信頼実行環境(TEE)によるシークレット管理
- コンテナ環境でパスワードやAPIキーのような シークレット管理 は常に課題
- ファイルシステムイメージにベイクせず、動的に注入する必要がある
- Dockerは ソケットフォワーディング をサポートしており、ローカルのドメインソケットをコンテナにマウントできる
- Docker for Mac/Windows の場合は Linux VM までソケットをフォワードする
ssh-agentのような鍵管理システムを、鍵そのものを直接公開せずにコンテナ内で利用できる
- ソケットフォワーディングは基本的な保護レベルを提供するが、拡大するソフトウェアサプライチェーン内のマルウェアに対しては より多くの防御レイヤー が必要
- ハイパーバイザー保護 をコンテナランタイム内に直接適用し、コンテナ間をまたぐ保護レベルを向上
- 信頼実行環境(TEE): 現代のCPUのハードウェア機能により、ホストOS上でもシークレットデータを保護 できる「機密VM(confidential VMs)」を作成可能
- アプリ、カーネル、ハイパーバイザーの境界をまたいでデータアクセス制限を適用できる
- ただし、TEEの構成・利用にはOS仮想化に似た 運用の複雑さ がある
- Confidential Containers ワーキンググループが、TEE内で実行されDockerで管理されるアプリを開発中
- Docker CLI がデスクトップのローカルTEEからホストを経由し、リモートクラウドのTEE環境まで 暗号化メッセージをフォワード する
- 開発者は現地に行かなくても機密性の高いクラウド環境に認証でき、認証情報は デスクトップのエンクレーブに安全に保存 される
AIワークロード向けGPGPUサポート
- AIワークロードの台頭により、まったく新しい課題が登場した。機械学習ワークロードの大半は GPU 上で実行される
- 中核的な問題: GPUワークロードには 完全に一致するカーネルGPUドライバーとユーザー空間ライブラリ が必要だが、多数のコンテナは単一の共有カーネル上で動作する
- 2つのアプリが同じカーネルGPUドライバーの 異なるバージョン を要求すると、Dockerが本来解決しようとしていたのと同じ根本的な衝突が発生する
- Dockerは2023年3月から CDI(Container Device Interface) をサポート
- コンテナ起動時にファイルシステムイメージをカスタマイズする
- GPUデバイスファイルとGPU専用の動的ライブラリを バインドマウント し、
ld.soキャッシュを再生成する
- CDIは特定のGPUクラス・ベンダー間でのイメージ移植性を保証するが、異なるOS・ハードウェアブランド間では 完全にシームレスではない
- CDIが追加する動的ライブラリはそれぞれ異なるAPIを定義するため、コンテナの従来インターフェースである安定した LinuxシステムコールABI に匹敵するものがない
- Nvidia GPU向けのアプリがApple MシリーズCPUで動かしにくい理由は、GPU仮想化サポートが異なるハードウェア間で ベクトル命令を変換できるほど成熟していない ため
- コンテナコミュニティおよびGPUメーカーと協力し、GPU関連依存関係を管理する より柔軟で安全な方法 を開発中
- 移植可能なインターフェース イニシアチブが合意形成に収束することを期待
結論
- Dockerは2013年、開発者がより簡単にアプリをビルド・共有・実行できるようにすることを目標に始まった
- 現在では標準的なクラウドおよびデスクトップの開発ワークフローに深く統合され、世界中の 数百万の開発者が毎日利用 し、毎月 数十億件のリクエスト を処理している
- 相互運用のための標準を構築する 活発で多様なオープンソースコミュニティ を維持することが一貫した目標
- CNCF(Cloud Native Computing Foundation) が複数の中核コンポーネントの管理者の役割を担う
- Open Container Initiative(Linux Foundation傘下) がイメージフォーマットの管理者
- 今日ではこれらの要素の多くの実装が活発に使われており、クラウド、デスクトップ、自動車・モバイル・宇宙船 などのエッジ環境での展開も増えている
- 2025年時点で一般的な開発者ワークフローは、継続的テスト・デプロイ、IDE言語サーバー、エージェント型コーディングによるAI支援 を統合している
- Dockerの観点では、中核となる「ビルド・アンド・ラン」ワークフローは10年前と非常によく似ているが、多様な環境での摩擦を減らすシステム支援 は大幅に強化された
- Dockerを、開発者がより速くコードをデプロイできるよう支える 目に見えない伴走者 にすることが目標であり、現代のAIコーディングワークフローに合わせて スケーラブルに設計 している
1件のコメント
Hacker News の反応
Dockerが最初に登場したとき、またひとつの**「革新」という売り文句かと、うんざりしていた
中途半端なNoSQL、無条件のマイクロサービス化、あらゆる関数呼び出しをRPCに置き換える流れが嫌だった
それでも今では、その単純さのおかげで頻繁に使っている
ただし他人のコンテナ**はいまだに地獄だ。自分は少なくとも単純に保とうとするが、あまりに多くを詰め込みすぎて、本人ですらデプロイ手順を分かっていないように見える人もいる
今はLLMのおかげで、開発者自身も目を通していないコードがあふれ出す時代になった気がする
数え切れない試みがあったのに、結局Dockerfileが生き残った理由は柔軟性にある
従来の運用のようにファイルをコピーしてコマンドを実行する構造がなじみやすかったからだ
見た目は無骨でも、この素朴な柔軟性が今後も主流であり続けそうだ
もしみんながNixやBazelを使っていたら、
docker buildは笑いものになっていたかもしれない異なるビルドでハッシュが変わると信頼できないので、ファイルの更新時刻のような要素がハッシュに含まれる限り、完全な一貫性は難しい
個人的にはmkosiが好きだが、誰もが空のOSテンプレートから始めたいわけではない
たいていはpublic registryからpullしてprivate registryへpushするだけなので、ローカルイメージはほとんど使われない
少し非効率だが、変えるほど不便でもないようだ
Dockerが2013年のPyCon US Santa Claraで初めて公開されたのを覚えている
当時のYouTube発表動画を見ると、歴史的な瞬間だ
論文の出版時期と実際の発表時期が違っていて混乱があったようだが、だいたい13年前の話だ
“Twelve years of Docker containers”より“A decade of Docker”のほうが自然だった
HNの初期発表投稿を覚えている
当時はLXCやVagrantのような代替手段にうんざりしていたが、Dockerは本当に救世主のような存在だった
Dockerが1990年代のSLIRPというダイヤルアップ用ツールを再利用してファイアウォールを回避した点が興味深い
外部からの接続は受けられなかったが、NATの前身のような概念だった
Dockerを使ってみたかったが、使うたびに必要な機能が欠けていた
おそらくあまり一般的でない技術スタックを使っていたからだろう
「自分のPCでは動く」を業界標準にしたのがDockerの偉大さだ
今ではそのPC自体を本番環境へ**「配送」**する時代になった
以前はデプロイが巨大な手続きだったが、今ではファイルシステムそのものを配布できるようになった
今のコーディングエージェント革命も、あのときに似た文化的転換に感じる
10行のスクリプトで環境を再現できるなら、「マシンを配布する」という発想も悪くない
なぜこのアプローチがそこまで魅力的なのか、自分たちに問い直す必要がある
抽象化が常に勝つのは、根本問題を修正するのがあまりに難しいからだ
ネットワーキングには詳しくないが、Macでコンテナに別個のIPアドレスを持たせたい
ポートマッピングなしで
container_ip:80でアクセスしたいLinuxでは可能だったが、MacではVMを経由する必要があるので複雑だ
WireGuardベースの方法も試したが、Dockerのアップデートのたびに壊れる
公式にサポートされる方法があるといいのに
Tailscale Docker Extensionが設定を自動で処理してくれる
ポートマッピングを避けたい理由が動的ポートにあるなら、
--net=hostオプションとHost Networking設定を有効にしてみるといい2013年はDocker、Guix、NixOSがそろって登場したパッケージングの豊作年だった
関連論文を書いていてこの事実に気づいた
その後、これほど多くの優れたプロジェクトが同じ年に同時に現れたことがあったのか気になる
GuixやNixは今でもなお少数の利用者層にとどまっている
MLやAIがあらゆる場所に入り込み、イメージサイズが幾何級数的に大きくなっている
torchひとつだけでも数GBになる
昔の30MBイメージが懐かしい
レイヤー間でファイル共有ができず、無駄が大きい
そのため、ファイル単位のdedupeをサポートする代替registryを自作している
Dockerが企業向けセキュリティソフトを欺くためにVPNのように偽装していた点は本当に興味深かった
技術史として見ても面白い事例だ