コンテナ
Container ・ こんてな
カーネルを共有したまま、見える世界を切り取られたプロセスとしてアプリを一式で動かす実行環境。
✦ 30秒で試す — 航海OSを丸ごと入れていないのに、どこでも同じように動くホストの一覧で、箱の中の1番のプロセスはどう見える?予想してみる概要
コンテナは、アプリケーション本体と、それが動くのに必要なライブラリ・ランタイム・設定ファイルを「イメージ」という一つの荷物にまとめ、どのマシンでも同じように動かす技術です。荷物ごと運べば中身は変わらない — 物流のコンテナと同じ発想で、「手元では動くのに本番で動かない」という古典的な問題を解消します。
仮想マシンがハードウェアごと模倣してその上に OS を丸ごと載せるのに対し、コンテナはホストのカーネルを共有したまま、プロセスを「見える世界を切り取られた箱」の中で動かします。中のプロセスからは自分専用のマシンがあるように見えますが、ホストから見ればただのプロセスが並んでいるだけです。OS を含まない分イメージは軽く、起動は1秒前後。この軽さが、開発環境の統一から CI/CD、マイクロサービスまで、現代の開発の流れ全体の前提になりました。
2013年に登場した Docker がこの技術を一気に普及させたため、いまも「コンテナ=Docker」と思われがちです。ただしイメージの形式と実行方法は 2015年に OCI(Open Container Initiative)で標準化され、Docker はその実装の一つになりました。本番のクラスタでは containerd や CRI-O といった別の実行エンジン(コンテナランタイム)が主流で、Docker で作ったイメージはそのままそこで動きます。
なぜ生まれたか
コンテナ以前、アプリケーションのデプロイは「環境差分との戦い」でした。開発者の手元、テスト環境、本番サーバで OS のバージョンやライブラリが微妙に異なり、「こっちでは動くのに」が絶えません。手順書に沿った手作業のセットアップは再現性が低く、仮想マシンで環境ごと固める方法はあったものの、数 GB のイメージと数十秒〜数分の起動時間は、日に何度も作っては捨てる開発の流れには重すぎました。
一方で、1つのカーネルの上でプロセスを隔離する試みは古くからありました。1979年の Unix の chroot(プロセスから見えるファイルの根を差し替える)に始まり、FreeBSD の jail(2000年)、Solaris Zones(2004年)と続きます。Linux では、プロセスから見える資源の範囲を種類ごとに切り分ける namespaces(2002年のマウントから順に追加)と、CPU やメモリの使用量に上限をかける cgroups(Google の技術者が開発し 2008年に取り込み)がそろい、2008年には両者を組み合わせた LXC が登場していました。
材料はそろっていたのに広まらなかったのは、使うのが難しかったからです。Docker の功績は、それらを「イメージを作る・共有する・動かす」という開発者向けの体験に包み直したことでした。Dockerfile という数十行のテキストで環境構築をコードにし、できたイメージをレジストリ経由で誰とでも共有できる — 環境そのものをビルドの成果物として配れるようにしたことが転換点でした。
詳細
隔離の仕組み — 見える世界を切り取られたプロセス
コンテナの実体は、カーネルの機能で囲われた普通のプロセスです。namespaces は「見えるもの」を切り分けます。PID 名前空間の中では自分が PID 1 から始まるプロセス一覧しか見えず、マウント名前空間ではイメージから展開したファイルシステムが / に見え、ネットワーク名前空間では自分専用の IP アドレスとポート番号の空間を持ちます。ほかにもホスト名(UTS)、プロセス間通信(IPC)、ユーザー ID の対応(user)などの名前空間があります。cgroups は「使える量」を制限し、メモリの上限を超えたプロセスはコンテナの中だけで強制終了(OOM Kill)されます。
ハードウェアの模倣もゲスト OS の起動も要らないため、仮想マシンと比べて圧倒的に軽量です。裏返せば、カーネルを共有している分だけ隔離は浅く、カーネルの脆弱性を突かれるとホスト側に影響が及び得ます。コンテナの中の root がそのままホストの root であることも多いので、root で動かさない、不要な権限(capabilities)を落とす、といった対策が基本です。強い境界が要る場面では、gVisor や Kata Containers、Firecracker のように、コンテナの使い勝手のまま小さな仮想マシンで包む方式が使われます。
同じプロセスが、中から見るか外から見るかで違う姿になることを、下の部品で確かめてください。視点をコンテナとホストで切り替えると、プロセスの番号・/ の中身・ネットワークがまるで違って見えます。ホストの視点からコンテナの PID 1 を止めると、コンテナそのものが止まります。
いまはコンテナ web の中から見ています。見えるのは自分のプロセスだけで、nginx は PID 1 です。ホストや db の箱を押して視点を変えてみてください。
PID 1 という特別な役目
コンテナの中で最初に起動したプロセスは PID 1 になり、コンテナの寿命はこのプロセスの寿命と一致します。PID 1 が終われば、その名前空間の残りのプロセスもすべて終わり、コンテナは停止します。ここに実務の落とし穴があります。Linux の PID 1 は、終了シグナル(SIGTERM)の既定の動作が無効になるなど特別扱いされるため、シェルスクリプト経由でアプリを起動するとシグナルがアプリに届かず、停止のたびに強制終了(SIGKILL)まで待たされることがあります。親を失ったプロセスの後始末(ゾンビの回収)も PID 1 の仕事です。アプリを exec で PID 1 にする、あるいは tini のような小さな init を PID 1 に置くのが定石です。
イメージとレイヤ
コンテナイメージは、Dockerfile の命令(ベースイメージの指定、依存のインストール、アプリのコピーなど)を1ステップずつ積み重ねた読み取り専用の「レイヤ」の集まりです。起動したコンテナは、その上に薄い書き込み用のレイヤを1枚重ね、変更はそこにだけ書かれます(コピーオンライト)。同じベースイメージを使うコンテナはレイヤを共有するので、ディスクもダウンロードも節約できます。レイヤはキャッシュされ、変更のない部分は再ビルドされないため、変わりにくいもの(依存のインストール)を先に、変わりやすいもの(アプリのコード)を後に書くのが速いビルドのコツです。
イメージはイミュータブル(作ったあとは書き換えない)なので、同じイメージからは何度でも同じコンテナが立ち上がります。設定を変えたいときは稼働中のコンテナに入って直すのではなく、イメージを作り直して置き換える。この運用が、サーバごとに手作業の変更が積み重なって誰も再現できなくなる「秘伝のタレ化」を防ぎます。
ビルドからデプロイまでの流れ
コンテナ運用の基本サイクルは「ビルド → レジストリへプッシュ → 各環境でプル・実行」です。レジストリ(Docker Hub、Amazon ECR、GitHub Container Registry など)はイメージ置き場で、Git がコードの共有拠点であるように、レジストリは実行環境の共有拠点として働きます。この流れは CI/CD のパイプラインに組み込まれるのが標準で、「push したらイメージがビルドされ、テストを通ったら本番に配られる」という自動化の単位そのものになっています。
実務での使いどころと落とし穴
コンテナがまず効くのは開発環境の統一です。新しいメンバーが docker compose up の一発でデータベース込みの開発環境を手に入れられます。次に CI でのテスト実行環境、そして本番のデプロイ単位。1コンテナ=1つの役割に絞る設計が定石で、この粒度の細かさがマイクロサービスと相性抜群です。小さなサービスを個別にビルド・デプロイ・スケーリングする運用は、コンテナなしには現実的ではありませんでした。
コンテナが増えるほど「どのマシンに何台配置するか」「落ちたら誰が立て直すか」という新しい問題が生まれます。多数のコンテナの群れを自動運用する基盤が Kubernetes です。落とし穴としては、イメージの肥大化(ビルドツールまで本番イメージに含めてしまう。マルチステージビルドで分ける)、コンテナ内にデータを保存して停止とともに失う(永続化はボリュームに逃がすのが原則)、latest タグの運用で「どの版が動いているか」が分からなくなる、ベースイメージに含まれる脆弱性を放置する、が典型です。
試問
1台の Linux(ホスト)の上で、コンテナ web(nginx)と db(postgres)が動いています。視点を「コンテナの中」と「ホスト」で切り替えると、見えるプロセスの一覧(ps)が変わり、一覧の ✕ でプロセスを止められます。止まったコンテナの箱を押すと起動し直せます。
いまはコンテナ web の中から見ています。見えるのは自分のプロセスだけで、nginx は PID 1 です。ホストや db の箱を押して視点を変えてみてください。
