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.log1. 「消える」を体験する
# 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 volume Docker 管理エリア( /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 webbind mount の本質
ホストの特定のディレクトリを、コンテナ内のパスに直接マウントする。両方向で同期する(ホストの変更がコンテナに、コンテナの変更がホストに反映)。
書式:
-v <ホストパス>:<コンテナパス>新しい書式:
--mount type=bind,source=<ホストパス>,target=<コンテナパス>ユースケース
- 開発中のソースコード反映: コードを書き換えるとコンテナ内のアプリも更新される(ホットリロードで爆速開発)
- 設定ファイルの差し込み:
-v ./nginx.conf:/etc/nginx/nginx.conf:roで設定を外から渡す- ローカルファイルの加工: 変換スクリプトをコンテナに任せる
bind mount の落とし穴
- 絶対パス必須:
-v src:/dataは named 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 mydatanamed 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
# コンテナ停止と同時に消える(ディスクに書かれない)
exittmpfs の本質
メモリ上に一時ファイルシステムを作る。書き込みは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 本番でハマる
対策
- named volume を使う(Docker が UID 管理してくれる)
- ホスト側で chown を合わせる:
sudo chown -R 999:999 /path/to/data- コンテナ側のユーザーを変える:
docker run --user $(id -u):$(id -g) ...- 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. セッション録画を終了
exit2. 今日の発見
- 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 mount | named volume | tmpfs |
|---|---|---|---|
| 実体の場所 | ホストの任意パス | Docker 管理領域 | メモリ |
| 永続性 | ホストにある間は永続 | volume が消えるまで永続 | コンテナ停止で消える |
| ホストからアクセス | 直接 ls/編集できる | できない(Mac は VM内) | できない |
| パフォーマンス | Linux は速い、Mac は遅い | Linux/Mac 共に良好 | 最速(メモリ) |
| 主な用途 | 開発時のホットリロード、設定ファイル投入 | 本番DBデータ、Redis永続化 | キャッシュ、機密ファイル、/tmp |
| 本番OK? | 設定ファイル限定でOK、データはNG | OK | OK(一時データ) |
| バックアップ | ホストのファイルとして簡単 | 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 を締めくくる。