Docker 基礎 #4 ボリュームとネットワーク — データと通信

#3 でコマンド群を掴んだら、この記事は データの永続性コンテナ間の通信 — 二つの運用テーマに入ります。

Docker 基礎 シリーズでこの記事の位置:

コンテナのファイルシステムは揮発性 #

まず一つの事実から。コンテナの中で作ったファイルはコンテナが消えると一緒に消えます。

確認は難しくありません。

揮発性のデモ
docker run --rm -it ubuntu:24.04 bash
root@xxx:/# echo "hello" > /tmp/note.txt
root@xxx:/# cat /tmp/note.txt
hello
root@xxx:/# exit

# もう一度入ると
docker run --rm -it ubuntu:24.04 bash
root@yyy:/# cat /tmp/note.txt
cat: /tmp/note.txt: No such file or directory

新しいコンテナはイメージから作り直された綺麗なインスタンスなので、前のコンテナで作ったファイルを知りません。これがコンテナの核心特性で — 再現性 の源泉です。同じイメージから立ち上げたコンテナはどこで立ち上げても同じだという保証です。

問題は DB データ、アップロードされた画像、ログのように生き残らなければならないもの です。こういうデータをコンテナの外に置く方法が ボリューム (volume) です。

二つのマウント — bind mount vs named volume #

Docker が提供する二つの永続化方式:

二つのマウント
┌─────────────────────────────────────────────────────────┐
│                      Host                               │
│  ┌──────────────────┐      ┌────────────────────────┐   │
│  │ /Users/me/data   │      │ Docker-managed area    │   │
│  │  (自分が管理)    │      │  (Docker が管理)       │   │
│  └────────┬─────────┘      └────────┬───────────────┘   │
│           │ bind mount              │ named volume      │
│           ▼                         ▼                   │
│   ┌─────────────────────────────────────────────────┐   │
│   │            Container — /app/data                │   │
│   └─────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘

bind mount はホストの 特定のパス をコンテナの中にそのまま挿す方式。ホストとコンテナが同じディレクトリを共有します。

named volume は Docker が自分で管理する領域にデータを置いて、その領域をコンテナに挿す方式。ホストの特定のパスに縛られていません。

bind mountnamed volume
ホストパス自分で指定Docker 管理領域 (/var/lib/docker/volumes/)
バックアップ・移動ホストディレクトリを動かすだけdocker volume コマンドまたは docker run
権限ホストユーザーの権限のままDocker が処理してくれる
主な用途開発 — コードのホットリロード運用 — DB データ、アップロード
OS の可搬性パスが OS ごとに違う同じように動作

基準を一行で: 開発では bind mount、運用では named volume。

bind mount — コードのホットリロード #

#2 の Flask アプリをまた持ってきます。コードを一文字変えるたびにイメージを作り直すのは非効率です。bind mount でホストのコードディレクトリをコンテナの中に挿せば、ホストで保存するだけでコンテナが変わったコードを読みます。

bind mount
docker run --rm -it \
  -p 8000:8000 \
  -v $(pwd):/app \
  hello-docker

-v $(pwd):/app の意味:

  • $(pwd) — ホストのカレントディレクトリ
  • : で区切り
  • /app — コンテナの中のマウントポイント (WORKDIR と同じ場所)

これで app.py をホストのエディタで編集して保存すると、コンテナの中の /app/app.py も即時に変わります。Flask の dev サーバーを --reload で起動すれば自動再起動まで。

--mount — より明示的な形 #

-v の代わりに --mount を使うと意味がもっとはっきりします。

--mount の形
docker run --rm \
  -p 8000:8000 \
  --mount type=bind,source=$(pwd),target=/app \
  hello-docker

-v は一行で軽く、--mount はオプションをキー・値で広げて書くので可読性が良い。新しく書くなら --mount 推奨、ただし短いコマンドには -v が便利なので両方よく見ます。

読み取り専用 #

データを保護したい場合は :ro または readonly を付けます。

読み取り専用 bind mount
docker run --rm -v $(pwd)/config:/etc/myapp:ro myapp

設定ファイルをコンテナに注入するときによく使うパターンです。

named volume — 運用データ #

DB コンテナのようにデータが生き残らなければならない場合は named volume です。

named volume を作る
docker volume create pgdata

これを PostgreSQL コンテナにマウント:

postgres + named volume
docker run -d --name pg \
  -e POSTGRES_PASSWORD=secret \
  -v pgdata:/var/lib/postgresql/data \
  postgres:16

-v pgdata:/var/lib/postgresql/datapgdata がホストパスではなく ボリューム名 という点が違いです。(Docker は : の前が絶対パスなら bind、名前なら named volume と解釈します。)

これでコンテナを消してもう一度作り直してもデータは生き残ります。

コンテナ再作成、データは維持
docker rm -f pg
docker run -d --name pg \
  -e POSTGRES_PASSWORD=secret \
  -v pgdata:/var/lib/postgresql/data \
  postgres:16
# 同じデータをそのまま見る

ボリュームコマンド群 #

ボリュームを扱う
docker volume ls               # ボリューム一覧
docker volume inspect pgdata   # メタデータ、ホストパスなど
docker volume rm pgdata        # 削除 (使われていないときだけ)
docker volume prune            # 使われていないボリュームを一括整理

docker volume inspect で見ればホストのどこにデータがあるかが出ます。普通は /var/lib/docker/volumes/pgdata/_data。macOS / Windows では Docker Desktop の VM の中なのでホストから直接アクセスは難しいですが、named volume はその違いを気にしなくても動作が一貫します。

匿名ボリューム #

-v /var/lib/postgresql/data のように 名前なし でマウントすると Docker が任意の名前で作ります。これが匿名ボリュームです。コンテナを消すとき docker rm -v で一緒に消さないとずっと溜まります。運用では必ず named で使ってください。

コンテナネットワーク一周 #

通信。Docker デーモンはデフォルトで三つのネットワークを作っておきます。

デフォルトネットワーク
docker network ls
# NETWORK ID    NAME      DRIVER    SCOPE
# xxx           bridge    bridge    local
# yyy           host      host      local
# zzz           none      null      local
モード意味
bridge (デフォルト)Docker が作った仮想ブリッジにコンテナがつながる。別の IP を受け取る
hostコンテナがホストネットワークスタックをそのまま使う。隔離が減る
noneネットワークなし。外部とどんな通信もしない

ほとんどの場合は bridge の上で物事が起きます。ただし — デフォルト bridge とユーザー定義 bridge は動作が違います。

デフォルト bridge の落とし穴 #

docker run だけだとデフォルト bridge にぶら下がりますが、このネットワークではコンテナ同士を名前で呼び合えません。 IP アドレスでしかアクセスできません。IP はコンテナが立ち上がり直すたびに変わるので不便です。

動かない例 (デフォルト bridge)
docker run -d --name web nginx
docker run -d --name app myapp

docker exec app ping web
# ping: bad address 'web'

代わりに ユーザー定義 bridge ネットワーク を作るとコンテナ名が自動的に DNS のように動作します。

ユーザー定義ネットワーク
docker network create mynet

docker run -d --name web --network mynet nginx
docker run -d --name app --network mynet myapp

docker exec app ping web
# 64 bytes from web (172.18.0.2) ...  ← 動く

web という名前が DNS で解決されます。コンテナの IP が変わっても、名前は変わらないので安定した参照になります。

実務ルール: 一緒に通信しなければならないコンテナは常に同じユーザー定義ネットワークに置く。Docker Compose はこれを自動でやってくれます — Compose ファイルのサービスは同じネットワークに入って、お互いをサービス名で呼べます。

よく使うネットワークコマンド #

ネットワークコマンド群
docker network create mynet              # 作る
docker network ls                        # 一覧
docker network inspect mynet             # どのコンテナがつながっているか
docker network connect mynet myapp       # 立ち上がっているコンテナに追加接続
docker network disconnect mynet myapp    # 切り離し
docker network rm mynet                  # 削除
docker network prune                     # 使われていないネットワークを一括整理

ポートマッピング — -p を深く #

-p の形はいくつかあります。

-p のバリエーション
-p 8000:8000              # ホスト 8000 → コンテナ 8000
-p 80:8000                # ホスト 80 → コンテナ 8000
-p 127.0.0.1:8000:8000    # ホストの 127.0.0.1 のみ (外部アクセスを遮断)
-p 8000                   # ホストの任意ポート → コンテナ 8000

運用でよく使うパターンが 127.0.0.1 バインディング です。

ローカルからのみアクセス可能
docker run -d -p 127.0.0.1:5432:5432 postgres:16

-p 5432:5432 だけだと全インターフェースで 5432 が開くので、ホストがインターネットに公開されていれば外部からも届きます (普通はファイアウォールが防ぎますが、安全装置は一枚多く)。DB のように外部に開く必要がないものは 127.0.0.1 バインディングが安全です。

macOS/Windows ユーザーへの注意: Docker Desktop 環境では -p がホスト OS のポートに直接マッピングされます。だからホストの 5432 がすでに別の PostgreSQL で使われていると衝突します。そんなときはホストポートを変えます (-p 5433:5432)。

コンテナ間通信は「内部ポート」 #

ここで一度混乱します。

セットアップ
docker network create mynet
docker run -d --name pg --network mynet \
  -e POSTGRES_PASSWORD=secret postgres:16     # -p なし
docker run -d --name app --network mynet \
  -e DB_HOST=pg -e DB_PORT=5432 myapp

pg コンテナに -p を渡さなくても app からは pg:5432 でアクセスできます。同じネットワークの中ではコンテナの内部ポートがそのまま見える からです。-p はホスト ↔ コンテナのマッピングであって、コンテナ間通信とは別です。

これが大事な理由: 運用で DB コンテナには -p を渡さないのが定石。外部ホストポートを開けず、同じネットワークのアプリコンテナからのみ届かせる形です。

host ネットワークと none モード #

たまに見るバリエーション。

host ネットワーク
docker run --rm --network host nginx

--network host はコンテナがホストのネットワークスタックをそのまま使います。-p マッピングが不要。ただし 隔離が減り、macOS/Windows では動作が違います (Docker Desktop の VM 境界のため)。Linux ホストの特殊な運用シナリオ以外ではあまり使いません。

none — ネットワークなし
docker run --rm --network none alpine sh

--network none は外部と通信しない隔離されたコンテナです。CTF、セキュリティ隔離、オフライン処理のような場面でたまに登場します。

診断 — 通信が通らないとき #

Docker ネットワークトラブルシューティングの第一歩:

診断順序
# 1. 同じネットワークか
docker inspect myapp --format '{{json .NetworkSettings.Networks}}'

# 2. コンテナの中で DNS 解決されるか
docker exec myapp getent hosts pg

# 3. ポートが開いているか
docker exec myapp nc -zv pg 5432
# nc がない slim イメージが多いので、busybox を一度立ち上げて確認することも
docker run --rm --network mynet busybox nc -zv pg 5432

# 4. コンテナ自体のポートマッピング確認
docker port pg

ほとんどの「届かない」はネットワーク分離 / コンテナ名のタイポ / コンテナが立ち上がっていない / アプリが 0.0.0.0 ではなく 127.0.0.1 にバインドしている — この四つのどれかです。

0.0.0.0 vs 127.0.0.1 を一段落 #

よく出会う落とし穴。コンテナの中のアプリが 127.0.0.1 にバインドすると コンテナの中だけで そのポートが開きます。ホストや別のコンテナから届きません。コンテナの中のサーバは常に 0.0.0.0 にバインド しないと外部からの接続を受けられません。#2 の Flask の例で host="0.0.0.0" を使った理由がこれです。

まとめ #

この記事で掴んだ絵:

  • コンテナファイルシステムは揮発性。生き残らなければならないデータは ボリューム でコンテナの外に置く
  • bind mount はホストパスをそのまま挿す — 開発のホットリロードに適している
  • named volume は Docker 管理領域 — DB データのような運用データに適している
  • ユーザー定義 bridge ネットワーク ではコンテナ名が DNS のように動作 — 常にユーザー定義ネットワークを使う
  • -p はホスト ↔ コンテナのマッピング、コンテナ間通信とは別 (内部ポートそのまま)
  • DB のようなコンテナは -p なしで立ち上げて外部公開を防ぎ、同じネットワークのアプリからのみアクセスさせる
  • コンテナの中のサーバーは 0.0.0.0 にバインド

次の記事 (#5 レジストリ — Docker Hub, GHCR, push/pull) では作ったイメージを別のマシンでも使えるように — レジストリにアップロードして取ってくる 流れを扱います。Docker Hub、GitHub Container Registry、そしてイメージ名とタグのルールまで整理します。

X