Dockerの仕組みを“やさしく深く”理解する:コンテナが「軽いVM」ではない理由を図解で解説【Infrastructure & Cloud】

Infrastructure & Cloud

🐳【初心者向け】Dockerの仕組みをやさしく解説:コンテナはなぜ「軽いVM」ではないのか?

Dockerを勉強していると、よくこんな説明を見かけます。

「Dockerは、軽量な仮想マシンのようなもの」

確かに、最初のイメージとしては分かりやすい説明です。

しかし、Dockerコンテナと仮想マシン(VM)は、 内部で動いている仕組みがかなり違います。

Dockerを単なる「軽いVM」として覚えてしまうと、 イメージ、コンテナ、Linuxカーネル、Namespace、cgroupsといった概念が登場したときに、 それぞれの関係が見えにくくなってしまいます。

この記事では、まず初心者向けに 「Dockerとは何をしている仕組みなのか?」 を整理します。

その後の深掘り編では、LinuxのNamespaceやcgroups、 イメージレイヤー、Copy-on-Writeなど、 コンテナの正体にもう一段深く潜っていきます。


結論:Dockerコンテナは「小さなVM」ではない

最初に結論です。

Dockerコンテナは、基本的には「隔離されたプロセス」です。

仮想マシンでは、物理マシンの上に仮想的なマシンを作り、 その中でゲストOSを動かします。

物理マシン
  ↓
Hypervisor
  ↓
仮想マシン
  ↓
Guest OS
  ↓
Application

一方、一般的なLinuxコンテナでは、 ホスト側のLinuxカーネルを共有しながら、 プロセスなどを隔離します。

Host OS
  ↓
Linux Kernel
  ↓
┌─────────┬─────────┐
│Container A│Container B│
│ Process   │ Process   │
└─────────┴─────────┘

つまりDockerは、 OSを丸ごともう1つ起動しているわけではありません。

この違いが、Dockerを理解する最初の重要ポイントです。


そもそもDockerとは何か?

Dockerは、アプリケーションとその実行に必要な環境を コンテナとして扱いやすくするためのプラットフォームです。

たとえばJavaアプリケーションを動かす場合、 環境によって次のような違いが発生することがあります。

  • Javaのバージョンが違う
  • 必要なライブラリが入っていない
  • OS側の設定が違う
  • 環境変数が違う

すると、よくある問題が発生します。

「自分のPCでは動くのに、本番環境では動かない」

Dockerでは、アプリケーションの実行に必要なファイルや設定を イメージとしてまとめることができます。

Application
   +
Runtime
   +
Library
   +
設定
   ↓
Docker Image

このイメージをもとにコンテナを起動することで、 環境差による問題を減らしやすくなります。


Dockerイメージとコンテナの違い

Docker初心者が最初に混乱しやすいのが、 「イメージ」と「コンテナ」の違いです。

かなり単純化すると、 次のように考えると分かりやすくなります。

Dockerfile
    ↓
  build
    ↓
Docker Image
    ↓
   run
    ↓
Container

Dockerイメージ

コンテナを作るための読み取り専用のテンプレートです。

コンテナ

イメージをもとに作成され、 実際にアプリケーションのプロセスを実行する環境です。

料理にたとえるなら、 イメージが「レシピ」だけというより、 調理に必要な材料や環境をまとめた実行用パッケージに近い存在です。

そして、そのイメージから実際に起動したものがコンテナです。


Dockerfileは何をしているのか?

Dockerfileは、 Dockerイメージをどのように作るかを記述するファイルです。

たとえば、非常に単純化したJavaアプリなら次のように書けます。

FROM eclipse-temurin:21-jre

COPY app.jar /app/app.jar

WORKDIR /app

CMD ["java", "-jar", "app.jar"]

それぞれの意味をざっくり見ると、

  • FROM:土台となるイメージ
  • COPY:ファイルをイメージへ追加
  • WORKDIR:作業ディレクトリを指定
  • CMD:コンテナ起動時のデフォルトコマンド

となります。

Dockerfileそのものがコンテナなのではありません。

Dockerfile
   ↓ build
Image
   ↓ run
Container

この流れをまず押さえておきましょう。


仮想マシン(VM)との違い

Dockerを理解するうえで、 VMとの比較はとても役立ちます。

仮想マシンの場合

┌────────────────┐
│ Application A  │
├────────────────┤
│ Guest OS       │
└────────────────┘

┌────────────────┐
│ Application B  │
├────────────────┤
│ Guest OS       │
└────────────────┘

        ↓

    Hypervisor

        ↓

     Host

それぞれのVMが、 独自のゲストOSとカーネルを持ちます。

コンテナの場合

┌───────────┐
│ Container A │
│    App A    │
└───────────┘

┌───────────┐
│ Container B │
│    App B    │
└───────────┘

       ↓

 Linux Kernel

       ↓

     Host

一般的なLinuxコンテナでは、 ホストのLinuxカーネルを共有します。

そのためVMのように、 コンテナごとに完全なゲストOSを起動する必要がありません。


なぜコンテナは起動が速いのか?

ここまで理解すると、 Dockerコンテナの起動が比較的速い理由も見えてきます。

VMでは、

VM起動
 ↓
Guest OS起動
 ↓
各種サービス起動
 ↓
Application起動

という処理が必要になります。

一方、コンテナでは基本的に、 既存のホストカーネル上で隔離されたプロセスを開始します。

Container起動
 ↓
隔離環境を準備
 ↓
Application Process起動

ゲストOSそのものを毎回ブートする必要がないため、 一般にVMより軽量かつ高速に起動できます。

ただし、実際の起動時間はイメージサイズやアプリケーションの初期化処理などにも左右されます。


Dockerを使うメリット

環境を揃えやすい

開発環境
 ↓
同じImage

テスト環境
 ↓
同じImage

本番環境
 ↓
同じImage

同じイメージを各環境で利用することで、 環境差を小さくできます。

依存関係を分離しやすい

たとえば、

Container A
Java 17

Container B
Java 21

のように、異なるランタイムを利用するアプリケーションを 同じホスト上で扱いやすくなります。

作成・破棄しやすい

コンテナ自体へ重要な状態を抱え込まない設計にすることで、 必要になったら作り、不要になったら捨てるという運用がしやすくなります。

これはKubernetesやECSなど、 コンテナオーケストレーションを理解するときにも重要な考え方です。


ここまでのまとめ(初心者向け)

ここまでで最も大切なのは、 「Docker=小さな仮想マシン」ではない という点です。

  • Dockerはコンテナを扱うためのプラットフォーム
  • コンテナの中ではアプリケーションなどのプロセスが動く
  • 一般的なLinuxコンテナはホストのLinuxカーネルを共有する
  • イメージはコンテナを作るためのテンプレート
  • Dockerfileからイメージを作り、イメージからコンテナを起動する
  • VMのようにコンテナごとのゲストOSを起動するわけではない

初心者向けとしては、まずここまで理解できれば十分です。

ここから先は、 「では、普通のLinuxプロセスをどうやってコンテナのように見せているのか?」 という内部構造へ進みます。


▼▼ 深掘り編ここから ▼▼


コンテナの正体は「隔離されたLinuxプロセス」

Dockerコンテナを理解するうえで、 非常に重要なポイントがあります。

コンテナを特別な種類のプロセスとして考える必要はありません。

Linux上では、コンテナ内のアプリケーションも ホストカーネルによって管理されるプロセスとして動いています。

では、普通のプロセスと何が違うのでしょうか。

そこで登場する代表的な仕組みが、 Namespaceとcgroupsです。

Container
   │
   ├─ Namespace
   │    ↓
   │  「見える世界」を分離
   │
   └─ cgroups
        ↓
      「使える資源」を制御

この2つを押さえると、 コンテナの正体がかなり見えてきます。


Namespace:プロセスから見える世界を分離する

LinuxのNamespaceは、 プロセスから見えるシステムリソースを分離する仕組みです。

代表的なNamespaceには次のようなものがあります。

Namespace 主に分離するもの
PID プロセスID空間
NET ネットワーク
MNT マウントポイント
UTS ホスト名など
IPC プロセス間通信
USER ユーザーID・グループID

たとえばPID Namespaceを利用すると、 コンテナ内部からは、 そのNamespace内のプロセスだけが見えるような環境を作れます。

Host
├─ Process 100
├─ Process 200
└─ Container
     ├─ Process 1
     └─ Process 2

コンテナ内ではプロセスIDが「1」に見えていても、 ホスト側では別のPIDを持つプロセスとして存在します。

つまり、 別のOSが動いているのではなく、見える世界が分離されている というわけです。


cgroups:使えるCPUやメモリを制御する

Namespaceだけでは、 プロセスから「何が見えるか」は分離できますが、 CPUやメモリを好き放題使われてしまう可能性があります。

そこで使われるのが、 cgroups(Control Groups)です。

cgroupsを利用すると、 プロセス群に対してCPUやメモリなどのリソース利用を制御・管理できます。

Host Resources
CPU    8 Core
Memory 16 GB

       ↓ cgroups

Container A
CPU / Memory 制限

Container B
CPU / Memory 制限

Dockerで指定する、

docker run --memory=512m ...

といった制限の背後では、 Linuxのcgroupsなどの仕組みが利用されています。

整理すると、

Namespace
   ↓
何が見えるかを分離

cgroups
   ↓
どれだけ使えるかを制御

という関係です。


Dockerイメージは「レイヤー」でできている

Dockerイメージにはもう一つ重要な特徴があります。

それがレイヤー構造です。

たとえば次のDockerfileを考えます。

FROM eclipse-temurin:21-jre

COPY app.jar /app/app.jar

WORKDIR /app

CMD ["java", "-jar", "app.jar"]

Dockerイメージは、変更内容を積み重ねた複数のレイヤーを利用して構成されます。

┌─────────────────┐
│ Application      │
├─────────────────┤
│ Runtime / Files  │
├─────────────────┤
│ Base Image       │
└─────────────────┘

この仕組みによって、 共通するレイヤーを再利用できます。

たとえば複数のイメージが同じベースイメージを利用している場合、 共通するデータを効率よく扱える可能性があります。


Copy-on-Writeとは?

イメージのレイヤーは基本的に読み取り専用です。

では、起動したコンテナでファイルを書き換えた場合はどうなるのでしょうか。

そこで関係する考え方が、 Copy-on-Write(CoW)です。

Container
┌─────────────────┐
│ Writable Layer   │ ← コンテナ側の変更
├─────────────────┤
│ Image Layer      │
├─────────────────┤
│ Image Layer      │
└─────────────────┘

コンテナ起動時には、 イメージの読み取り専用レイヤーの上に 書き込み可能なレイヤーが追加されます。

ファイルへの変更が必要になると、 ストレージドライバーの仕組みに応じて、 元のイメージそのものを書き換えるのではなく、 コンテナ側のレイヤーで変更を扱います。

これによって同じイメージから、 複数の独立したコンテナを起動できます。

       Image
      ↙     ↘
Container A Container B
   ↓           ↓
変更A        変更B

元となるイメージは共有しながら、 それぞれの変更は分離できます。


なぜコンテナに重要なデータを保存しないのか?

Dockerでは、 コンテナ内の書き込み可能レイヤーだけに 永続化したいデータを保存する設計は避けるのが基本です。

コンテナを削除すると、 そのコンテナ固有の書き込みレイヤーも失われるためです。

Container
   ↓ 削除

Writable Layer
   ↓
消える

そこで永続化したいデータには、 Volumeなどを利用します。

Container
    │
    ↓
  Volume
    │
    ↓
Persistent Data

コンテナ本体とデータのライフサイクルを分離することで、 コンテナを作り直しても必要なデータを残せます。


コンテナの「PID 1」が特別な理由

もう一段だけ深く潜ってみましょう。

コンテナを起動すると、 コンテナのPID Namespace内でメインとなるプロセスが PID 1として動きます。

Container
   │
   └─ PID 1
       └─ Application

LinuxではPID 1には、 通常のプロセスとは少し異なる振る舞いや役割があります。

特にコンテナ運用では、

  • シグナルを正しく受け取れるか
  • 子プロセスを適切に回収できるか
  • 終了要求に対してアプリが正常終了できるか

といった点が重要になります。

たとえばコンテナ停止時には、 アプリケーションへ終了シグナルを伝え、 一定時間内に安全に終了させたい場合があります。

そのためDockerfileのCMDやENTRYPOINTの書き方、 アプリケーション側のシグナル処理なども、 実運用では重要になってきます。


「コンテナは安全な箱」とは限らない

コンテナはプロセスを隔離できますが、 VMとまったく同じ境界を持つわけではありません。

Linuxコンテナではホストカーネルを共有するためです。

Container A ─┐
             │
Container B ─┼─ Linux Kernel
             │
Container C ─┘

そのため、

  • 不要な権限を与えない
  • root権限を必要以上に使わない
  • イメージに秘密情報を埋め込まない
  • 不要なLinux capabilitiesを与えない
  • イメージや依存パッケージの脆弱性を管理する

といった対策が重要になります。

「コンテナに入れたから安全」ではない という点は覚えておきましょう。


Dockerを理解するとKubernetesやECSも見えやすくなる

Dockerの仕組みを理解すると、 その先に登場するコンテナオーケストレーションも理解しやすくなります。

Docker Image
     ↓
 Container
     ↓
大量に運用したい
     ↓
┌────────────────┐
│ Kubernetes      │
│ Amazon ECS      │
│ その他の基盤    │
└────────────────┘

コンテナが数個なら手動でも管理できます。

しかし、数十・数百と増えてくると、

  • どのサーバで動かすか
  • 落ちたコンテナをどう復旧するか
  • 何個起動しておくか
  • どう負荷分散するか
  • どう更新するか

といった問題が発生します。

これらを自動的に管理する仕組みが、 KubernetesやAmazon ECSなどの コンテナオーケストレーションです。

つまり、

コンテナを理解する
      ↓
Dockerが分かる
      ↓
ECS / Kubernetesが分かる
      ↓
クラウドネイティブ設計が見えてくる

という流れで知識がつながっていきます。


Dockerの内部構造をまとめると

Dockerコンテナの裏側を大きく整理すると、 次のようになります。

Docker
  │
  ├─ Image
  │    └─ Layer
  │
  └─ Container
       │
       ├─ Process
       │
       ├─ Namespace
       │    └─ 見える世界を分離
       │
       ├─ cgroups
       │    └─ リソースを制御
       │
       └─ Writable Layer
            └─ 変更を保持

Dockerがゼロから仮想化技術を作っているわけではありません。

Linuxが持っているさまざまな機能と、 コンテナイメージやランタイムなどの仕組みを組み合わせ、 コンテナを作成・配布・実行しやすい形にまとめている と考えると、本質が見えやすくなります。


Deep Friendly Tech の一言(後書き)

Dockerを最初に学ぶと、 「軽量な仮想マシン」という説明はとても便利です。

しかし、その説明からもう一歩進んで、

VM
= マシンを仮想化する

Container
= プロセスの実行環境を隔離する

という違いを意識すると、 Dockerの景色がかなり変わります。

さらに、

Namespace
= 見える世界

cgroups
= 使える資源

Image Layer
= 実行環境の積み重ね

までつながれば、 「Dockerコマンドの使い方」から一歩進んで、 なぜコンテナがそのように動くのかを考えられるようになります。

Dockerは黒い箱ではありません。

箱の壁を少し透かしてみると、 その奥ではLinuxのプロセス管理が静かに働いています。🐧

タイトルとURLをコピーしました