1-6. ボリューム - データを永続化する

所要時間: 40-50分(がっつりなら2セッション分) ゴール: コンテナのエフェメラル性を理解し、bind mount / named volume / tmpfs を使い分けられる コミット内容: 永続化を試した操作ログを ~/log/docker_day06.log に保存


このレッスンのゴール

  • コンテナのエフェメラル性を「家畜 vs ペット」のメタファで説明できる
  • bind mount / named volume / tmpfs を場面別に使い分けられる
  • bind mount の UID/GID 衝突問題(権限地獄)の原因と対処を知っている
  • --volumes 付き prune の破壊力を理解している
  • 開発: bind、本番DB: named、一時: tmpfs の選択基準が言える

なぜ学ぶか(実務悩みベース)

  • 「DB データが消えた!」事故を未然に防ぐ
  • 開発時に「ホストでファイル編集→コンテナで即反映」をやりたい
  • 本番のステートフルなコンテナ(DB、キュー)を安全に扱う
  • 権限エラー(Permission denied)で半日溶かす定番ハマりを避ける

前章とのつながり

1-3 で「コンテナの書込層は CoW で作られる」と知った。今回はその書込層の 外側 にデータを置く話。docker rm で消えない場所を作る。


大前提: コンテナの中のデータは「消える」

前々回で Copy-on-Write の話をした。コンテナ内でファイルを書き込むと、それは 「書込層」 に書かれる。そして:

コンテナを docker rm で削除すると、書込層も一緒に消える

例えば:

docker run -d --name db1 -e POSTGRES_PASSWORD=secret postgres:16
# ... DB に大事なデータを書き込む ...
docker rm -f db1
# ↑ データ全消失

「いやそれは怖い」と思うはず。実際、そのままだと本番では使えない。コンテナは「短命であることが前提」の仕組みだから、データだけ別の場所に置く必要がある。

コンテナのエフェメラル性

エフェメラル(ephemeral)= 「短命の、はかない」。

コンテナは:

  • 頻繁に作って捨てる前提: スケール、デプロイ、バグ修正のたびに新しいコンテナへ
  • 状態を持たない設計(Stateless)が美徳
  • 永続データは外部に分離する(DB、S3、ボリュームなど)

この考え方は「12factor」「Cloud Native」の中核。コンテナを「ペット」(個別に手入れする)ではなく「家畜」(数だけ管理、壊れたら作り直す)として扱う。

このレッスンでは、その「外部の保存場所」をどう Docker に組み込むかを学ぶ。


セッション①: ボリュームの3種類(25-30分)

0. 作業準備

mkdir -p ~/log ~/learn/docker/day06
cd ~/learn/docker/day06
script ~/log/docker_day06.log

1. 「消える」を体験する

# alpine を起動、ファイルを作って、消す
docker run --rm -it --name vol_test alpine sh -c '
  echo "important data" > /data.txt
  cat /data.txt
'
# 出力: important data
 
# もう一度同じイメージから起動
docker run --rm -it alpine sh -c 'cat /data.txt'
# 出力: cat: can't open '/data.txt': No such file or directory
# → 前回作ったファイルは消えている(コンテナごとに書込層は別物)

コンテナごとに書込層は別

同じイメージから起動しても、コンテナごとに 独立した書込層 が作られる。前のコンテナで書いたファイルは、別コンテナからは見えない。

これを解決するのが ボリューム

2. 3種類のボリューム

┌──────────────────────────────────────────────────────┐
│                  ホストマシン                        │
│                                                      │
│  ┌────────┐   ┌────────────────┐   ┌──────────┐   │
│  │ /Users │   │ Docker          │   │  RAM     │   │
│  │ /takato│   │ /var/lib/docker │   │          │   │
│  │ /...   │   │ /volumes/...   │   │          │   │
│  └───┬────┘   └────────┬───────┘   └────┬─────┘   │
│      │                 │                │          │
└──────┼─────────────────┼────────────────┼──────────┘
       │                 │                │
   bind mount       named volume       tmpfs
       │                 │                │
       ▼                 ▼                ▼
   ┌────────────────────────────────────────┐
   │           コンテナ                     │
   │   /host_files   /data    /tmp_data     │
   └────────────────────────────────────────┘

3種類の使い分け

種類データの実体使い所
bind mountホストの任意のパス開発(ソースコードを反映)
named volumeDocker 管理エリア(/var/lib/docker/volumes本番(DBデータなど)
tmpfsメモリ(RAM)一時データ、シークレットの一時保持

3. bind mount - ホストのパスを直接マウント

# ホスト側に作業ディレクトリを用意
mkdir -p ~/learn/docker/day06/src
echo "<h1>From host!</h1>" > ~/learn/docker/day06/src/index.html
 
# nginx を起動、ホストの src を /usr/share/nginx/html にバインド
docker run -d --name web \
  -p 8080:80 \
  -v ~/learn/docker/day06/src:/usr/share/nginx/html \
  nginx
 
# 確認
curl http://localhost:8080/
 
# ホスト側でファイルを編集
echo "<h1>Updated!</h1>" > ~/learn/docker/day06/src/index.html
 
# 再リクエスト → 即座に反映!
curl http://localhost:8080/
 
# 片付け
docker rm -f web

bind mount の本質

ホストの特定のディレクトリを、コンテナ内のパスに直接マウントする。両方向で同期する(ホストの変更がコンテナに、コンテナの変更がホストに反映)。

書式: -v <ホストパス>:<コンテナパス>

新しい書式: --mount type=bind,source=<ホストパス>,target=<コンテナパス>

ユースケース

  • 開発中のソースコード反映: コードを書き換えるとコンテナ内のアプリも更新される(ホットリロードで爆速開発)
  • 設定ファイルの差し込み: -v ./nginx.conf:/etc/nginx/nginx.conf:ro で設定を外から渡す
  • ローカルファイルの加工: 変換スクリプトをコンテナに任せる

bind mount の落とし穴

  • 絶対パス必須: -v src:/datanamed volume として解釈される。ホストのパスは ./src か絶対パスで
  • 権限問題: コンテナ内の UID とホストの UID が違うと「Permission denied」事故。詳細は後述
  • 本番では使わない: ホストのパス構造に依存するので、別マシンに持っていけない。本番はnamed volume

4. named volume - Docker 管理のボリューム

# ボリュームを作成
docker volume create mydata
 
# 一覧
docker volume ls
 
# 詳細
docker volume inspect mydata
 
# 使う(ボリューム名:コンテナ内パス)
docker run -d --name db \
  -e POSTGRES_PASSWORD=secret \
  -v mydata:/var/lib/postgresql/data \
  postgres:16
 
# DB にデータを入れる
docker exec -it db psql -U postgres -c "
  CREATE TABLE t (id int);
  INSERT INTO t VALUES (1), (2), (3);
"
 
# コンテナを丸ごと削除
docker rm -f db
 
# 新しいコンテナを同じボリュームで起動
docker run -d --name db2 \
  -e POSTGRES_PASSWORD=secret \
  -v mydata:/var/lib/postgresql/data \
  postgres:16
 
# データが残っているか確認
docker exec -it db2 psql -U postgres -c "SELECT * FROM t;"
# → 1, 2, 3 が出る!データは生きている
 
# 片付け
docker rm -f db2
docker volume rm mydata

named volume の本質

Docker が管理するストレージ。ホストの特定パスではなく、/var/lib/docker/volumes/<名前>/_data のような Docker 内部の場所に作られる。

書式: -v <ボリューム名>:<コンテナパス>

ユースケース

  • DB のデータ永続化: postgres、MySQL、Redis などのデータ
  • アプリのアップロードファイル保存
  • 本番運用: ホストのパス構造に依存しないのでポータブル

特徴

  • Docker が管理してくれる(バックアップ用APIもある)
  • 削除されない限り残る(docker rm してもボリュームは残る)
  • docker volume rm で明示的に削除

bind mount と named volume の自動判定

-v ./src:/data       # ホストパス(./で始まる)→ bind mount
-v /Users/me/src:/data  # 絶対パス → bind mount
-v mydata:/data      # 名前だけ(パスでない)→ named volume(自動作成)

慣れるまでは --mount type=bind,... / --mount type=volume,... の方が明示的で安全。

5. tmpfs - メモリ上の一時データ

# /tmp_data を tmpfs(RAM)にする
docker run --rm -it \
  --tmpfs /tmp_data:size=64m \
  alpine sh
 
# 中で:
df -h /tmp_data    # tmpfs として認識
echo "secret" > /tmp_data/key.txt
# コンテナ停止と同時に消える(ディスクに書かれない)
exit

tmpfs の本質

メモリ上に一時ファイルシステムを作る。書き込みはRAM、コンテナ停止で消失。

ユースケース

  • シークレットの一時保持: 平文のキーを暗号化前に置く場所など
  • 大量の一時ファイル: コンパイルの中間ファイル、テストのテンポラリ
  • 書き込み高速化: ディスクI/Oが遅いケース

落とし穴: RAM を食う。size で上限を指定しないと「ホストのメモリ全部使う」事故に


セッション②: 永続化パターンと落とし穴(25分)

6. 「開発」と「本番」でのボリューム使い分け

開発時 vs 本番時

開発時: bind mount でホットリロード

docker run -d \
  -v $(pwd)/src:/app/src \
  -v $(pwd)/package.json:/app/package.json \
  -p 3000:3000 \
  myapp:dev

ソース変更が即反映 → 編集→再起動の手間がない。Compose と組み合わせるのが普通(後の章)。

本番時: named volume でデータ永続化

docker run -d \
  -v pgdata:/var/lib/postgresql/data \
  -e POSTGRES_PASSWORD=$DB_PASS \
  postgres:16

ホストのパスに依存しない、Docker 公式の方法でデータを守る。

7. データ永続化のパターン: バックアップとリストア

named volume のデータをバックアップする方法。

# テスト用ボリュームに何か入れる
docker volume create backup_test
docker run --rm -v backup_test:/data alpine sh -c "
  echo 'data 1' > /data/file1.txt
  echo 'data 2' > /data/file2.txt
"
 
# バックアップ: ヘルパーコンテナでボリュームをマウントして tar に固める
docker run --rm \
  -v backup_test:/data:ro \
  -v $(pwd):/backup \
  alpine tar czf /backup/backup.tar.gz -C /data .
 
ls -lh backup.tar.gz
 
# リストア: 新しいボリュームに展開
docker volume create restored_data
docker run --rm \
  -v restored_data:/data \
  -v $(pwd):/backup \
  alpine tar xzf /backup/backup.tar.gz -C /data
 
# 確認
docker run --rm -v restored_data:/data alpine ls /data
docker run --rm -v restored_data:/data alpine cat /data/file1.txt
 
# 片付け
docker volume rm backup_test restored_data
rm backup.tar.gz

ボリュームバックアップのパターン

ボリュームに直接アクセスはできない(Docker内部のパス)ので、「ヘルパーコンテナでマウントして tar を作る」 のが定番。

本番では:

  • 定期的にこの方法で tar を作って S3 に送る(cron / systemd timer)
  • DB なら pg_dump / mysqldump などDBネイティブのバックアップを優先
  • ボリュームをスナップショットできるストレージ(AWS EBS、GCE PD)を使うなら、そっちが楽

8. パーミッション地獄(最重要の落とし穴)

# ホストに root が所有のファイルを作る(操作ミスとして)
mkdir -p ~/learn/docker/day06/vol_test
sudo chown root:root ~/learn/docker/day06/vol_test
 
# Node アプリのコンテナにマウント(node ユーザーで動く)
docker run --rm \
  -v ~/learn/docker/day06/vol_test:/app \
  node:20-alpine sh -c "
    whoami
    ls -la /app
    touch /app/new_file.txt
  "
# Permission denied で書き込めない、というのが典型
 
# 片付け
sudo rm -rf ~/learn/docker/day06/vol_test

コンテナ内ユーザー UID とホスト UID の問題

bind mount は ホストのファイル所有者(UID/GID)をそのまま見せる。コンテナ内のプロセスが別ユーザーで動いていると、書き込み権限が無い事故が起きる。

典型例:

  • Node の公式イメージ: node ユーザー(UID 1000)で動く設定があり、ホスト側の root 所有ファイルに書けない
  • postgres イメージ: postgres ユーザー(UID 999)で動く。ホスト側の bind mount 先の所有者が違うと起動失敗
  • macOS は別: Docker Desktop が裏で UID マッピングを吸収してくれるので、ハマりにくい。でも Linux 本番でハマる

対策

  1. named volume を使う(Docker が UID 管理してくれる)
  2. ホスト側で chown を合わせる: sudo chown -R 999:999 /path/to/data
  3. コンテナ側のユーザーを変える: docker run --user $(id -u):$(id -g) ...
  4. Dockerfile で USER root にする(セキュリティ的にはアンチパターン)

9. 読み取り専用マウント(:ro)

# 設定ファイルを「絶対に書き換えられたくない」時
docker run -d \
  -v $(pwd)/nginx.conf:/etc/nginx/nginx.conf:ro \
  -p 8080:80 \
  nginx

:ro (read only) フラグ

マウントを読み取り専用にする。コンテナが書き込もうとするとエラーになる。

使い所

  • 設定ファイルを差し込む時: アプリが万一書き換えてしまう事故を防ぐ
  • セキュリティ強化: 「このコンテナはこのデータを読むだけ」を強制
  • シークレットファイルのマウント: 書き換え禁止が原則

10. ボリュームの全削除(お掃除)

# 一覧
docker volume ls
 
# 未使用ボリュームを一括削除(停止コンテナも参照していないもの)
docker volume prune
 
# 特定のボリュームを削除
docker volume rm myvolume
 
# 強制(コンテナが使っていても)
docker volume rm -f myvolume

docker system prune --volumes の威力

全部を一括掃除するコマンド。Mac の Docker Desktop の容量が逼迫した時の最終手段。

docker system prune -a --volumes

これを叩くと:

  • 停止中のコンテナ全削除
  • 未使用イメージ全削除
  • 未使用ネットワーク削除
  • 未使用ボリューム削除(データ消失リスク)

「未使用」の判定が間違うと大事なDBデータを消す事故 が起きる。本番では絶対に叩かない。


練習課題

cd ~/learn/docker/day06
 
# 1. ホストのファイルをbind mountして nginx に見せる
mkdir -p site
echo "<h1>$(date)</h1>" > site/index.html
 
docker run -d --name w1 \
  -p 8080:80 \
  -v $(pwd)/site:/usr/share/nginx/html:ro \
  nginx
 
curl http://localhost:8080/
 
# 2. ホスト側でファイルを書き換えて、反映を確認
echo "<h1>更新: $(date)</h1>" > site/index.html
curl http://localhost:8080/
 
# 3. named volume で永続化を体験
docker volume create note_data
docker run --rm -v note_data:/data alpine sh -c "echo 'hello volume' > /data/memo.txt"
# コンテナ削除(--rm)→ ボリュームは生きてる
docker run --rm -v note_data:/data alpine cat /data/memo.txt
 
# 4. tmpfs で消えることを確認
docker run --rm --tmpfs /tmp_data:size=10m alpine sh -c "
  echo 'temp' > /tmp_data/test.txt
  cat /tmp_data/test.txt
"
# ↑ 終わったら消える
 
# 5. お片付け
docker rm -f w1
docker volume rm note_data
rm -rf site

試して観察してみる

  • bind mount したファイルをホスト側で削除したら、コンテナからどう見える?
  • named volume はホストのどこに実体がある?(Mac の場合、Docker Desktop の VM内なのでホストOSからは見えない)
  • --tmpfs で書いたファイルがメモリ上にあることを df でどう確認する?

締め: 振り返り(10分)

1. セッション録画を終了

exit

2. 今日の発見

- bind mount と named volume、どんな時にどっち?:
- パーミッション問題でハマったら、何を確認する?:
- 自分のアプリで「永続化すべきデータ」は何?:
- 詰まったところ:

やらかし事例: ボリュームの惨事集

事例1: docker volume prune で本番 DB データを溶かす

「使ってないボリューム掃除しよう」と本番サーバーで docker volume prune 実行 → 一時停止していた DB コンテナのボリュームが「未使用」判定で削除。本番では絶対に prune しない。必ず volume を 名前指定 で扱う。

事例2: bind mount で本番に開発ソースを混入

docker-compose の volumes: - ./app:/app をそのまま本番に持っていき、ホストの編集が本番に直結。本番では bind mount 禁止、named volume か焼き付け済みイメージのみ。

事例3: UID/GID 不一致で Permission denied

コンテナ内のプロセスが UID 1000 で動いているが、bind mount したホストディレクトリが UID 501 所有 → 書き込み不可。docker run --user $(id -u):$(id -g) か、Dockerfile 側で UID を合わせる。

事例4: Mac の bind mount が遅い

macOS の Docker Desktop は VM 経由なので bind mount が桁違いに遅い。Node の node_modules を bind すると死ぬほど遅い。:delegated / :cached か、開発専用 named volume に分離。

事例5: tmpfs に大事なデータを書く

tmpfs はメモリ上なので コンテナ停止で必ず消える。「DB の WAL は速いように tmpfs」とかやると、再起動でデータ消失。

物語: 「コンテナはペットではなく家畜」の出自

「Cattle, not Pets」というスローガンは、2012年頃の OpenStack コミュニティで提唱された。それ以前のサーバー運用は:

  • ペット: 1台ごとに名前を付け、症状が出たら手で治す。再構築は最終手段。
  • 家畜: 名前は番号、症状が出たら殺処分してすぐ新しい個体に置き換える。

物理サーバー時代は仕方なくペット運用だった。クラウド + コンテナ時代では、コンテナは家畜である方が運用が安定する。「個別に手入れする」と障害復旧が遅くなる。

ただし、これは 状態を持たないアプリだから言える話。DB のような「状態そのもの」を持つコンテナでは、状態を コンテナの外 に切り出す必要がある。そのための仕組みがボリューム。

「コンテナを家畜にするには、データを別場所に隔離する」これがボリュームの存在意義。

対比表: bind mount vs named volume vs tmpfs

観点bind mountnamed volumetmpfs
実体の場所ホストの任意パスDocker 管理領域メモリ
永続性ホストにある間は永続volume が消えるまで永続コンテナ停止で消える
ホストからアクセス直接 ls/編集できるできない(Mac は VM内)できない
パフォーマンスLinux は速い、Mac は遅いLinux/Mac 共に良好最速(メモリ)
主な用途開発時のホットリロード、設定ファイル投入本番DBデータ、Redis永続化キャッシュ、機密ファイル、/tmp
本番OK?設定ファイル限定でOK、データはNGOKOK(一時データ)
バックアップホストのファイルとして簡単docker run --rm -v vol:/data alpine tar...不可(揮発)

自己評価チェックリスト

知識レベル

  • エフェメラル性を「家畜 vs ペット」で説明できる
  • 3種類のボリュームを場面別に使い分けられる
  • パーミッション問題の原因(UID不一致)を即答できる
  • docker volume prune の危険性を理解している

実行レベル

  • コンテナを削除するとデータが消える、を実体験で確認した
  • bind mount でホストのファイル変更が即コンテナに反映されることを見た
  • named volume でデータが生き残ることを確認した
  • tmpfs の挙動を見た
  • :ro で読み取り専用マウントができる

メタ認知

  • 自分のアプリで「永続化すべきデータ」を全部洗い出した
  • 次に Compose を書くとき、どの volume を使うかを決めた

詰まった時のチートシート

やりたいことコマンド
bind mount-v $(pwd)/dir:/app/dir
読み取り専用 bind-v $(pwd)/conf:/etc/conf:ro
named volume-v myvol:/data
tmpfs--tmpfs /tmp:size=64m
明示的書式--mount type=bind,source=...,target=...
ボリューム作成docker volume create <名前>
ボリューム一覧docker volume ls
ボリューム詳細docker volume inspect <名前>
ボリューム削除docker volume rm <名前>
ボリューム一括削除docker volume prune
UID 指定で起動docker run --user $(id -u):$(id -g) ...

「実務OK」基準

  • 3種類のボリュームを使い分けられる: 開発=bind、本番DB=named、一時=tmpfs
  • bind mount の権限問題が起きた時に原因を特定できる
  • named volume でDBコンテナを再作成してもデータが残るパターンを書ける
  • --volumes 付き prune の危険性を腹落ちしている

ここまで来たら、Level 1 のラスト。実際にアプリをコンテナ化して動かす総復習をやる。


次のレッスン

1-7. Level 1 総復習 - Go アプリをコンテナ化する へ。

Hello World HTTP サーバーを Go で書き、Dockerfile でコンテナ化、ビルド、起動、停止、再起動、ログ確認、ボリュームでの永続化まで一通りやって、Level 1 を締めくくる。