
コンテナ
コンテナは、アプリケーションとその実行に必要なライブラリ、依存関係、設定ファイルなどを1つのパッケージにまとめた、軽量で独立した実行環境です。従来の仮想マシン(VM)が各VMごとに独自のゲストOSとカーネルを持つのに対し、コンテナはホストOSのカーネルを共有しながら動作するため、起動が数秒と高速で、リソース使用量もMB単位と非常に軽量です。
軽量性を実現しながらも、コンテナ間の適切な分離を保つために、Linuxカーネルの名前空間(namespace)とcgroups(Control Groups)という2つの重要な機能を活用しています。
名前空間は、PID、ネットワーク、ファイルシステム、ホスト名など、プロセスが見ることができるシステムリソースの範囲を制限することでプロセス分離を実現し、各コンテナが独立した環境として動作できるようにします。
cgroupsは、CPU、メモリ、ディスクI/Oなどのシステムリソースの使用量を制限・監視する機能で、例えば docker run --memory=512m --cpus=1.5 のように指定することで、特定のコンテナがホストのリソースを独占することを防ぎます。
コンテナの作成、起動、停止、削除などのライフサイクル管理を担うのがコンテナランタイムで、Dockerの場合、ユーザーが操作するDocker CLIから、高レベル機能を提供するDocker Daemon(dockerd)、コンテナ管理を行うcontainerd、そして実際にnamespaceとcgroupsを設定してコンテナプロセスを起動する低レベルランタイムのruncまで、階層化されたアーキテクチャで構成されています。
このように、コンテナは仮想マシンと比較してカーネル共有によるオーバーヘッド削減を実現しながらも、名前空間とcgroupsによる適切な分離とリソース管理、そしてコンテナランタイムによる標準化されたライフサイクル管理により、軽量で高速かつ安全な実行環境を提供しています。
Dockerアーキテクチャ
Dockerはクライアント-サーバー型アーキテクチャを採用しており、複数のコンポーネントが連携してコンテナの実行環境を提供します。
Docker Engineは、Dockerの中核となるコンポーネントで、コンテナの作成、実行、管理を担う総合的なプラットフォームです。このDocker Engineは、主に3つの要素で構成されています。
Docker Client(docker CLI)は、ユーザーがコマンドラインから操作を行うためのインターフェースです。docker runやdocker buildといったコマンドを実行すると、Docker ClientはこれらのリクエストをDocker Daemonに送信します。
リクエストはREST APIを介して通信され、Docker ClientとDocker Daemon間のやり取りはHTTP/HTTPSプロトコルベースで行われます。これにより、ローカルだけでなくリモートのDocker Daemonとも通信が可能になります。
Docker Daemon(dockerd)は、バックグラウンドで常駐するサーバープロセスで、Docker Clientからのリクエストを受け取り、イメージ、コンテナ、ネットワーク、ボリュームなどのDockerオブジェクトを管理します。Docker Daemonは実際のコンテナ実行処理を直接行わず、下位のコンテナランタイムに処理を委譲します。
containerdは、Docker Daemonの下位に位置する高レベルのコンテナランタイムで、コンテナのライフサイクル管理(起動、停止、削除など)を担当します。containerdはイメージの管理やストレージの操作も行い、Docker Daemonとruncの間の橋渡し役となります。
最下層のruncは、OCI(Open Container Initiative)仕様に準拠した低レベルのコンテナランタイムで、実際にLinuxカーネルの機能(namespace、cgroups等)を使用してコンテナプロセスを生成・実行します。runcは、containerdからの指示を受けて、コンテナの実体となるプロセスを起動します。
イメージとコンテナ
Dockerイメージは、アプリケーションの実行に必要なファイルシステムと設定情報をパッケージ化したテンプレートです。イメージはレイヤー構造で構成されており、各レイヤーは読み取り専用レイヤーとして積み重ねられています。例えば、ベースOSのレイヤー、ミドルウェアのレイヤー、アプリケーションのレイヤーといった具合に、下から順に重なり合って1つのイメージを形成します。
コンテナインスタンスは、このDockerイメージから実際に起動された実行環境です。イメージとコンテナの関係性は、オブジェクト指向プログラミングにおける「クラスとインスタンス」の関係に似ています。1つのイメージから複数のコンテナを起動でき、各コンテナは独立した実行環境として動作します。
コンテナ起動時、読み取り専用のイメージレイヤーの最上部に書き込み可能レイヤー(コンテナレイヤー)が追加されます。コンテナ内でファイルの作成や変更が行われると、その変更は全てこの書き込み可能レイヤーに記録されます。ここで重要な仕組みがCopy-on-Write(CoW)です。コンテナ内で既存ファイルを変更する際、元の読み取り専用レイヤーのファイルは変更されず、代わりにそのファイルが書き込み可能レイヤーにコピーされてから変更されます。これにより、複数のコンテナが同じイメージレイヤーを共有しながら、それぞれ独立した変更を保持できるため、ディスク容量の効率化と高速な起動が実現されています。
コンテナを削除すると書き込み可能レイヤーのデータは失われますが、元のイメージは変更されないため、同じイメージから何度でも同一の初期状態のコンテナを起動できます。この特性が、Dockerの「環境の再現性」と「ポータビリティ」を支える根幹となっています。
レジストリとリポジトリ
Dockerにおけるレジストリとは、Dockerイメージを保存・配布するための中央管理システムです。最も代表的なレジストリはDocker Hubで、公式イメージや世界中の開発者が公開したイメージを無料で利用できます。企業環境では、セキュリティやプライバシーの観点からプライベートレジストリを構築し、社内専用のイメージを管理することも一般的です。
レジストリ内では、イメージはリポジトリという単位で管理されます。リポジトリは特定のアプリケーションやサービスに関連するイメージをグループ化したもので、例えばnginxやmysqlといった名前で識別されます。各リポジトリ内には複数のバージョンが存在し、これらはタグによって区別されます。タグはnginx:latestやmysql:8.0のようにコロン(:)で区切って指定し、バージョンや環境(production、developmentなど)を示すために使用されます。
イメージの配布は、docker pushコマンドでレジストリにアップロードし、docker pullコマンドでダウンロードすることで行われます。この仕組みにより、開発環境で作成したイメージを本番環境に展開したり、チームメンバー間で同一の環境を共有したりすることが容易になります。
Docker Hubを使用する場合はdocker pull nginx:latestのように直接イメージ名を指定でき、プライベートレジストリの場合はdocker pull registry.example.com/myapp:1.0のようにレジストリのURLを含めた完全なイメージ名を指定します。
ライフサイクル
コンテナのライフサイクルは、まず作成(create)から始まります。docker createコマンドは、イメージからコンテナを作成しますが、この時点ではまだ起動していない状態です。作成されたコンテナはdocker startコマンドで起動(start)され、プロセスが実行を開始します。
作成と起動を同時に行う実行(run)コマンドが最もよく使われます。docker runは内部的にcreateとstartを組み合わせた処理を行い、一度のコマンドでコンテナを作成・起動します。
起動中のコンテナは、docker stopコマンドで停止(stop)できます。停止されたコンテナはメモリ上から解放されますが、コンテナ自体は保持されており、再度startで起動可能です。一時的に処理を中断したい場合は、docker pauseで一時停止(pause)することもできます。pauseはプロセスを凍結するだけなので、docker unpauseで即座に再開できます。
コンテナに問題が発生した場合や設定を再読み込みしたい場合は、docker restartで再起動(restart)します。これは内部的にstopとstartを連続して実行します。
不要になったコンテナはdocker rmで削除(rm)します。ただし、実行中のコンテナは削除できないため、事前に停止する必要があります(-fオプションで強制削除も可能)。
ストレージ
Dockerコンテナは、デフォルトではエフェメラル(一時的)な性質を持ち、コンテナを削除するとその中のデータも失われます。そのため、データの永続化やコンテナ間のデータ共有を実現するために、Dockerは3種類のストレージマウント方式を提供しています。
1. ボリューム(Volume)
Dockerが管理する永続化ストレージで、最も推奨される方式です。ボリュームはDockerが専用のディレクトリ(Linuxでは/var/lib/docker/volumes/配下)に保存し、Dockerコマンドで管理できます。コンテナのライフサイクルとは独立しており、コンテナを削除してもデータは保持されます。また、複数のコンテナから同じボリュームをマウントすることで、コンテナ間のデータ共有が可能です。バックアップや移行もDocker CLIを使って簡単に行えます。
2. バインドマウント(Bind Mount)
ホストマシンの任意のディレクトリやファイルをコンテナ内にマウントする方式です。ホスト側の絶対パスを指定して使用し、開発環境でソースコードをリアルタイムに反映させたい場合などに便利です。ホストのファイルシステムに直接アクセスするため、設定ファイルの配置や、ログファイルの出力先として利用されます。ただし、ホスト環境に依存するため、ポータビリティは低くなります。
3. tmpfs
メモリ上に一時的なストレージを作成する方式です。ディスクに書き込まれないため、機密情報を扱う場合や、高速な一時ファイル処理が必要な場合に使用します。コンテナが停止すると、tmpfs内のデータは完全に消失します。Linuxホストでのみ利用可能です。
ネットワーク
Dockerは複数のネットワークドライバを提供し、コンテナ間の通信やホストマシン、外部ネットワークとの接続を柔軟に制御できます。
ネットワークドライバの種類
ブリッジネットワークは、Dockerのデフォルトネットワークドライバです。単一ホスト上でコンテナを分離し、同じブリッジネットワーク内のコンテナ同士が通信できる仮想ネットワークを作成します。docker0という仮想ブリッジを経由して、コンテナ間通信が行われます。
ホストネットワークは、コンテナがホストマシンのネットワークスタックを直接使用するモードです。ネットワークの分離を行わず、コンテナとホストが同じIPアドレスとポート空間を共有します。ネットワークパフォーマンスが最大化されますが、ポートの競合に注意が必要です。
オーバーレイネットワークは、複数のDockerホストにまたがる分散ネットワークを構築します。Docker SwarmやKubernetesなどのオーケストレーションツールで使用され、異なる物理ホスト上のコンテナ間で安全な通信を実現します。VXLANトンネリング技術を使用して、複数ホスト間のコンテナ間通信を透過的に処理します。
ポートの公開と通信
コンテナを外部ネットワークからアクセス可能にするには、ポートマッピングを使用します。docker run -p 8080:80のように、ホストのポート(8080)とコンテナのポート(80)をマッピングすることで、外部からのアクセスを受け付けます。
ポート公開には、EXPOSEと-p/--publishの2つの概念があります。DockerfileのEXPOSE命令は、コンテナがどのポートでリッスンするかをドキュメント化する宣言的な指定であり、実際にはポートを公開しません。実際にポートを公開して外部アクセスを可能にするには、docker run -pオプション(publish)を使用する必要があります。-p 80:80で特定ポートを公開するか、-PオプションでEXPOSEされたすべてのポートをランダムなホストポートにマッピングできます。
同一ブリッジネットワーク内のコンテナ間通信では、コンテナ名をホスト名として使用できます。例えば、appコンテナからdbコンテナへはdb:3306のように接続でき、Dockerの内部DNSが名前解決を行います。これにより、IPアドレスの管理が不要になり、動的なコンテナ環境でも安定した通信が可能になります。
メリット
Dockerはポータビリティに優れ、開発環境、テスト環境、本番環境で同一のコンテナイメージを使用できるため、「ローカルでは動くが本番では動かない」といった問題を解消し、環境の一貫性を実現します。
従来の仮想マシン(VM)と比較して、ホストOSのカーネルを共有する仕組みにより軽量性が高く、コンテナイメージのサイズは数十MBから数百MB程度に抑えられ、高速起動が可能で数秒以内にアプリケーションを起動できます。
OSレベルの仮想化によりリソース効率が良く、同一ハードウェア上で多数のコンテナを実行でき、必要に応じてコンテナの数を増減させるスケーラビリティにも優れています。
Dockerイメージのビルド、プッシュ、デプロイを自動化できるため、DevOps/CI/CD統合が容易で、Jenkins、GitLab CI/CD、GitHub Actions等のツールと組み合わせることで、コード変更から本番環境へのデプロイまでを自動化し、開発サイクルを大幅に短縮できます。