深夜2時、PostgreSQLデータベースのデータが全消滅――volumesについての痛い教訓
あの日、production serverでPostgreSQLコンテナを再デプロイしようとdocker rm -f postgresを実行して作り直した。アプリを開いたら真っ白。3ヶ月分のデータが跡形もなく消えていた。
原因はシンプルで、だからこそ痛かった。データをコンテナの内部に保存していて、外部ではなかった。コンテナが削除されれば、データも一緒に消える。それ以来、volumeを確認せずにデータベースをデプロイすることは二度とない。
コンテナ内のデータが危険な理由
Dockerコンテナにはwritable layerと呼ばれるファイルシステム層がある。コンテナ内に書き込むものすべて(データベースファイル、アップロードファイル、ログなど)はそこに保存される。問題は、この層がコンテナのライフサイクルに紐づいていることだ。コンテナが消えれば、データも消える。
docker stop→ データは残る(コンテナが停止するだけ)docker rm→ データが永久に消える- 新バージョンのデプロイ(imageの再ビルド、コンテナの再作成)→ データ消失
- サーバークラッシュ、Docker daemon再起動 → コンテナのephemeral状態が失われる可能性がある
Dockerにはコンテナ外にデータを保存する3つの方法がある: volumes、bind mounts、そしてtmpfsだ。productionにはvolumesが最も推奨される。Dockerが自動管理し、ホストのディレクトリ構造に依存しない。
始める前の基本概念
VolumesはDockerに標準搭載されており、追加インストールは不要だ。ただし2種類あり、その違いは思っている以上に重要だ。
Named volumes vs Anonymous volumes
Anonymous volume: Dockerがb3c2e1f9a8d7...のようなランダムな名前を自動生成する。追跡しにくく、忘れがちで、気づかないうちにディスクを圧迫してしまう。
# Anonymous volume — productionでは使用しない
docker run -v /var/lib/postgresql/data postgres
Named volume: 意味のある名前を自分で付けられ、管理しやすく、コンテナとは独立して存在する。
# Named volumeを作成する
docker volume create postgres_data
# 確認する
docker volume ls
docker volume inspect postgres_data
inspectの出力で、実際のデータがホスト上のどこに保存されているかがわかる。通常は/var/lib/docker/volumes/postgres_data/_dataだ。
productionからの実践的な設定例
1. Named volumeを使ったPostgreSQL
# Named volumeを使ってPostgreSQLを起動する
docker run -d \
--name postgres \
-e POSTGRES_PASSWORD=mysecretpassword \
-e POSTGRES_DB=myapp \
-v postgres_data:/var/lib/postgresql/data \
-p 5432:5432 \
postgres:16
# テスト: データを作成する
docker exec -it postgres psql -U postgres -d myapp -c \
"CREATE TABLE test (id serial, name text); INSERT INTO test (name) VALUES ('hello');"
# コンテナを削除する
docker rm -f postgres
# 再作成 — データはまだ残っている
docker run -d \
--name postgres \
-e POSTGRES_PASSWORD=mysecretpassword \
-e POSTGRES_DB=myapp \
-v postgres_data:/var/lib/postgresql/data \
-p 5432:5432 \
postgres:16
docker exec -it postgres psql -U postgres -d myapp -c "SELECT * FROM test;"
# 結果: データは残っている
2. Docker Compose内のVolumes
私のproductionスタックはapp server + PostgreSQL + Redis + ユーザーアップロード用volumeで構成されている。実際のインシデントを経て改善してきたCompose設定がこれだ。
services:
postgres:
image: postgres:16
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: myapp
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
command: redis-server --appendonly yes
restart: unless-stopped
app:
image: myapp:latest
volumes:
- uploads:/app/public/uploads
depends_on:
- postgres
- redis
volumes:
postgres_data: # Dockerが未作成の場合は自動で作成する
redis_data:
uploads:
ファイル末尾のvolumes:ブロックに注目してほしい。すべてのnamed volumesを宣言する場所だ。このブロックがない状態でserviceでvolumeを使うと、Composeはすぐにエラーを出す。
3. Bind mounts — 開発環境専用
Bind mountsはホストのディレクトリを直接コンテナにマップする。開発時のhot reloadや、ホストからconfigファイルを読む必要があるときに便利だ。
# Bind mount: カレントディレクトリを/appにマップする
docker run -v $(pwd):/app node:20 npm run dev
# Compose内 — 開発環境の設定
services:
app:
image: myapp:latest
volumes:
- ./src:/app/src # 開発用bind mount
- uploads:/app/uploads # データ用named volume
productionでデータベースやユーザーデータにbind mountを使うと不要なトラブルを招く。permissionが複雑で、ホスト側のpathが変わった瞬間にコンテナがエラーになる。
4. Volume drivers — ホスト外へのデータ保存
マルチサーバー構成やクラウドへの自動バックアップが必要なら、Volume driversを使えばNFS、S3、クラウドストレージにデータを保存できる。NFSの例:
docker volume create \
--driver local \
--opt type=nfs \
--opt o=addr=192.168.1.100,rw \
--opt device=:/srv/nfs/mydata \
nfs_data
小規模チームやシングルVPS環境では、ローカルのnamed volumeで十分だ。/var/lib/docker/volumes/ディレクトリを適切にバックアップするだけでよい。
確認とモニタリング
volumeの一覧と使用容量を確認する
# すべてのvolumeを一覧表示する
docker volume ls
# 特定のvolumeの詳細を確認する
docker volume inspect postgres_data
# 使用容量を確認する(root権限が必要)
du -sh /var/lib/docker/volumes/postgres_data/_data
# または docker system df で概要を確認する
docker system df -v
どのコンテナがどのvolumeを使用しているか調べる
# 実行中のコンテナが使用しているvolumeを確認する
docker inspect postgres --format='{{json .Mounts}}' | python3 -m json.tool
volumeのバックアップとリストア
深夜2時の事故の後、毎日のcronjobに追加したバックアップスクリプトだ。シンプルだが、これまで少なくとも2回は助けられた。
# volumeをtar.gzファイルにバックアップする
docker run --rm \
-v postgres_data:/data \
-v $(pwd)/backups:/backup \
alpine tar czf /backup/postgres_$(date +%Y%m%d_%H%M%S).tar.gz -C /data .
# バックアップからリストアする
docker run --rm \
-v postgres_data:/data \
-v $(pwd)/backups:/backup \
alpine tar xzf /backup/postgres_20240101_020000.tar.gz -C /data
未使用volumeのクリーンアップ
# どのコンテナも使用していないvolumeを一覧表示する
docker volume ls -f dangling=true
# すべてのdangling volumeを削除する — 注意: 元に戻せない
docker volume prune
# 特定のvolumeを削除する(先にコンテナを停止すること)
docker volume rm volume_name
注意: docker system pruneは--volumesフラグを付けない限りvolumeを削除しない。これはデータ保護のためのDockerの意図的な設計だ。productionの自動クリーンアップスクリプトにそのフラグを追加してはいけない。
ディスク使用量のモニタリング
PostgreSQL WAL logs、Redis RDB snapshots――これらは気づかないうちにディスクを食い尽くす。このチェックをモニタリングスクリプトに追加して、80%を超えたら警告するようにしている。
#!/bin/bash
# volumeがディスクの80%を超えた場合に警告する
DISK_USAGE=$(df /var/lib/docker/volumes | tail -1 | awk '{print $5}' | tr -d '%')
if [ "$DISK_USAGE" -gt 80 ]; then
echo "ALERT: Docker volumes disk usage at ${DISK_USAGE}%"
docker system df -v
fi
productionチェックリスト
- すべてのデータベース(PostgreSQL、MySQL、MongoDB、Redis)はnamed volumeを使用すること
- ユーザーアップロード、生成ファイル — named volume
- 重要なvolumeごとに最低1回/日のバックアップcronjobを設定すること
- 最低月1回はリストアテストを実施すること — テストしていないバックアップはバックアップではない
- バックアップなしでproductionに
docker system prune --volumesを使わないこと - volume名とどのコンテナが使用するかをドキュメント化すること — 3ヶ月後には確実に忘れている
深夜2時の教訓以来、新しいスタックをセットアップするたびにdocker inspectを実行して、データがwritable layerではなくvolumeに入っていることを確認している。そのわずか30秒が、深夜のデバッグセッションを少なくとも何度か防いでくれた。

