Docker Volumes: コンテナ再起動・削除後もデータを永続保存する方法

Docker tutorial - IT technology blog
Docker tutorial - IT technology blog

深夜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つの方法がある: volumesbind 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秒が、深夜のデバッグセッションを少なくとも何度か防いでくれた。

Share: