Docker Composeのbindとnamed volumeの使い分け
Docker Composeのbind mountとnamed volumeはどちらを選ぶべきですか。設定とデータの用途、権限エラーの落とし穴、確認、バックアップ、移行方法を解説します。
Bind mount と named volume: 簡単な答え
Docker Compose の volume には 2 種類あり、選択の基準はファイルを誰が管理するかです。自分で書き込みや読み取りを行うファイルには bind mount を使用します。設定、テンプレート、静的サイトなどが該当します。アプリケーションが管理するデータには named volume を使用します。データベースファイル、検索インデックス、アップロードされたメディアなどが該当します。bind mount は、エディターで開けるホスト上のパスを指します。named volume は Docker が作成して管理するストレージで、Docker 経由でアクセスします。
どちらもサービス内の同じ volumes: キーに記述するため、混同されやすくなります。違いはコロンの左側です。左側が . または / で始まる場合はホストパスなので、bind mount です。それ以外は名前なので named volume です。その名前は、トップレベルの volumes: ブロックでも宣言する必要があります。
compose ファイル内の 2 つの構文
services:
db:
image: postgres:17
environment:
POSTGRES_PASSWORD: changeme
volumes:
- pgdata:/var/lib/postgresql/data
web:
image: nginx:1.27
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./site:/usr/share/nginx/html:ro
volumes:
pgdata:pgdata:/var/lib/postgresql/data は名前付きボリュームです。./nginx.conf:/etc/nginx/nginx.conf は bind mount で、:ro は読み取り専用でマウントします。コンテナが書き換える必要のない設定には、これが適切なデフォルトです。トップレベルの volumes: エントリを忘れると、Compose は service "db" refers to undefined volume pgdata で停止します。
起動して、Docker が作成したものを一覧表示します。
docker compose up -d
docker volume lsボリューム名は pgdata ではありません。<project>_pgdata という名前になります。プロジェクト名のデフォルト値は、compose ファイルを格納しているディレクトリ名です。myapp というディレクトリでは、myapp_pgdata になります。これは、ディレクトリ名を変更すると新しい空のボリュームが作成され、アプリケーションがデータを失ったように見えるため重要です。実際には失われていません。古いボリュームは docker volume ls で引き続き一覧表示できます。ディレクトリを移動する可能性がある場合は、compose ファイルで name: を指定するか、COMPOSE_PROJECT_NAME を設定して名前を固定します。このような設定は、他の Compose の環境ファイルと Secret と一緒に管理します。
バインドマウントで権限エラーが主に発生する理由
これは実用上の大きな違いで、主な理由は 1 つのルールにあります。初回使用時に空の名前付きボリュームはイメージから初期化されますが、バインドマウントは初期化されません。
Docker がイメージ内ですでに内容を持つディレクトリに空の名前付きボリュームをマウントすると、その内容をイメージが設定した所有者とモードのままボリュームへコピーします。公式の Postgres イメージでは /var/lib/postgresql/data が専用の postgres ユーザーによって所有されるため、ボリュームも同じ数値 ID の所有になり、データベースが起動します。
この初期化が、名前付きボリュームに与えられる唯一の優位点です。Docker はボリュームの保存場所を管理しますが、コンテナは通常のファイルシステム操作で読み書きするため、バインドマウントと同様に所有者とモードが適用されます。イメージに存在しないパスへ名前付きボリュームをマウントした場合や、別のイメージまたはユーザーによって最初に書き込まれた場合、イメージから所有権は引き継がれません。そのため、そこで non-root プロセスが拒否されることもあります。以下の id の確認は、どちらの種類にも使用できます。
バインドマウントでは逆の動作になります。ホスト上にあるものが所有権を含めてそのままコンテナから見え、そのパスにあるイメージの内容は隠されます。ホストのディレクトリが存在しない場合、Docker daemon が作成します。daemon は root として実行されるため、root:root が所有するディレクトリになります。non-root ユーザーとして実行されるコンテナプロセスは、そこへ書き込めません。
PermissionError: [Errno 13] Permission denied: '/data/app.db'解決策は数値を一致させることです。バインドマウントの所有権は名前ではなく数値の user id で比較されます。これは、コンテナが独自の /etc/passwd を持つためです。コンテナ内の app というユーザー名は、ホスト上では意味を持ちません。Uid 1000 は、daemon が user-namespace remapping(userns-remap、/etc/docker/daemon.json または dockerd のコマンドラインで設定)を使用していない限り、両側で uid 1000 を意味します。remapping を使用すると、すべてのコンテナ ID がホスト上の subordinate range に移されます。231072 から始まる Docker の例の範囲では、コンテナの uid 0 はホストの uid 231072 になり、コンテナの uid 1000 はホストの uid 232072 になります。
id -u
mkdir -p ./data
docker compose exec web id
sudo chown -R 1000:1000 ./datadocker compose exec web id は、コンテナプロセスが実際に実行される uid を表示します。remapping がなければ、ホストのディレクトリをその数値に合わせるか、サービスで user: "1000:1000" を使ってコンテナを自分の数値に固定します。remapping が有効な場合、設定した ID も移動されるため、user: でコンテナをホストの ID に固定することはできません。この例の範囲では、user: "1000:1000" もホストの uid 232072 になります。代わりに、ホストのディレクトリをマッピング後のホスト ID に chown します。自分の範囲は /etc/subuid と /etc/subgid にあります。Docker のドキュメントでは、単一のコンテナで remapping を無効にする方法として docker container create、run または exec 上の --userns=host だけを記載しています。自分で作成したアプリケーションでは、user: を固定する方が簡潔です。自分で作成していないイメージでは、ホストのディレクトリを chown する方が安全です。イメージによっては、entrypoint を root として起動してから権限を下げ、特定の所有権を内部に要求するためです。
さらに 2 つ注意点があります。Fedora、RHEL などで SELinux(security-enhanced Linux)が enforcing の場合、バインドマウントは relabel するまで拒否されます。コンテナ間で共有するパスには :z を、1 つのコンテナだけが使用するパスには :Z を、- ./data:/data:Z として追加します。また、ディレクトリではなく単一ファイルをバインドマウントすると、エディターがファイルをその場で書き換えずに置き換えたときに動作しなくなります。マウントは元の inode を追跡するためです。コンテナには再起動するまで古い内容が表示され続けます。頻繁に編集するファイルの場合は、親ディレクトリをマウントしてください。
性能差が現れる場所
Linux サーバーでは、どちらも同じカーネル経路を通るため、スループットの差は小さく、この点だけを基準に選ぶ必要はありません。デフォルトの local ドライバーを使用する named volume は、/var/lib/docker/volumes/ 配下の Docker と同じファイルシステム上に配置されます。bind mount は、指定した場所に配置されます。
差が現れるのは、コンテナが仮想マシン内で実行される macOS および Windows の Docker Desktop です。そこでの bind mount は、ファイル共有層を介してホストのファイルシステムから仮想マシンへアクセスします。そのため、Node.js の依存関係ツリーや PHP フレームワークのキャッシュなど、多数の小さなファイル操作を行うワークロードでは、処理速度が明確に低下します。named volume は仮想マシン内に保持されるため、このコストがかかりません。これが、多くの開発用 compose ファイルでソースディレクトリを bind mount し、node_modules に対して named volume を宣言する理由です。
もう1つの実際の違いは、データの配置先です。/mnt/backup への bind mount では、データはそのディスクに配置されます。named volume は、/var/lib/docker を保持するファイルシステムに配置されます。VPS では通常、root ディスクです。named volume 内でデータベースが拡大すると、システムログと同じディスクを消費します。インシデントになる前に確認してください。
docker system df -v
df -h /var/lib/dockerdocker system df -v はすべての volume とそのサイズを一覧表示し、どのコンテナからも参照されなくなった volume に印を付けます。
名前付きボリュームの確認
名前付きボリュームはブラックボックスではありません。Docker に保存場所を確認させます。
docker volume inspect myapp_pgdataMountpoint フィールドには、実際のホスト上のパスが表示されます。通常は /var/lib/docker/volumes/myapp_pgdata/_data です。sudo ls で読み取れるため、簡単な確認に便利です。ただし、ファイルを編集する場所として扱わないでください。そこで root として書き込むと、前述の所有者問題が再発します。また、このパスは local ドライバー固有の詳細であり、他のボリュームドライバーには共通しません。
安全に内容を確認するには、ボリュームをマウントした使い捨てコンテナを使用します。
docker run --rm -v myapp_pgdata:/vol alpine ls -la /volこれは任意のドライバーで動作します。実際のコンテナと同じ権限を確認でき、--rm により後に何も残りません。
各種類のバックアップ
bind mount は通常のディレクトリなので、ファイル単位のバックアップツールでそのまま扱えます。ホスト側のパスをバックアップ対象に指定すれば完了です。named volume では、内部にアクセスするために追加の手順が必要です。volume とホスト側のディレクトリを同じ短時間だけ実行するコンテナにマウントし、アーカイブを書き出します。
docker run --rm \
-v myapp_pgdata:/data:ro \
-v "$PWD":/backup \
alpine tar czf /backup/pgdata.tar.gz -C /data .新しい volume に逆の手順で展開して復元します。
docker volume create myapp_pgdata_restored
docker run --rm \
-v myapp_pgdata_restored:/data \
-v "$PWD":/backup \
alpine tar xzf /backup/pgdata.tar.gz -C /datatar は、コンテナ内で root として実行する際に数値の所有権を保持します。これにより、復元した volume をアプリケーションから使用できます。
両方の種類に共通する注意点があります。データベースの実行中にファイルをコピーすると、変化し続ける状態のアーカイブになります。そのため、破損した状態で復元される可能性があります。先にサービスを停止するか、docker compose exec -T db pg_dump -U postgres appdb > appdb.sql のようにデータベース固有のツールでダンプしてください。これにより通常のファイルが作成されるため、compose ファイルとともに、通常の 暗号化された restic バックアップ手順に含められます。
bind mount を named volume に移行する
移行は名前の変更ではなくコピーです。所要時間は約 1 分です。
docker compose down
docker volume create myapp_pgdata
docker run --rm \
-v "$PWD/data":/from \
-v myapp_pgdata:/to \
alpine sh -c 'cp -a /from/. /to/'cp -a は所有者、モード、タイムスタンプを保持します。そのため、旧ディレクトリを読み取れたコンテナユーザーは、新しい volume も読み取れます。次に、サービスが pgdata:/var/lib/postgresql/data を使用するように変更し、トップレベルの volumes: ブロックに pgdata を追加します。その後、docker compose up -d を実行し、旧ディレクトリを削除する前にアプリケーションのログを確認します。逆方向に移行する場合は、同じコマンドで /from と /to を入れ替えます。
テスト中は、1 つだけ注意してください。docker compose down は named volume をそのまま残しますが、docker compose down -v はプロジェクトで宣言されているすべての named volume を削除し、元に戻せません。bind mount はどちらの場合も残ります。Docker がそのディレクトリを所有していないためです。ライフサイクル用コマンドにまだ慣れていない場合は、VPS 向け Docker Compose 基礎ガイドで手順を確認できます。
サービスごとに選ぶ
そのファイルを誰が書き込むかを確認します。テキストエディターで編集して git にコミットする設定は、バインドマウントに置きます。:ro でマウントすることで、ホストから参照でき、バージョン管理も可能になります。手作業で開くことのないアプリケーションの状態は、名前付きボリュームに置きます。Docker がイメージを基に権限を設定し、データがホスト上のパスに依存しないためです。
中間的なケースがメディアです。写真ライブラリはアプリケーションが書き込みますが、同時に自分で管理する対象でもあります。また、専用ディスクを割り当てたいほど大きくなることもあります。そのディスク上のパスにバインドマウントし、所有権を一度明示的に設定します。多くのセルフホスト構成では、このパターンに落ち着きます。データベースとキャッシュには名前付きボリュームを使い、設定と、重要な大容量ディレクトリにはバインドマウントを使います。VPS で稼働する Chatwoot のようなサポートデスクもまさにこの構成です。Postgres は名前付きボリュームに置き、アップロードされた添付ファイルはバックアップ対象として指定できるパスに置きます。
FAQ
バインドマウントと名前付きボリュームの違いは何ですか?
バインドマウントはホスト上のパスをコンテナ内に割り当てるため、両側から同じディレクトリが見え、通常のツールで編集できます。名前付きボリュームは Docker が作成・管理するストレージで、名前で参照し、トップレベルの volumes: ブロックで宣言します。実用上の使い分けは所有権です。自分で管理する設定にはバインドマウントを、アプリケーションが管理するデータには名前付きボリュームを使用します。
バインドマウントでは "permission denied" になるのに、名前付きボリュームではならないのはなぜですか?
空の名前付きボリュームはイメージから初期化されるため、通常はイメージが設定した所有権を引き継ぎ、コンテナユーザーが書き込めます。ただし権限は適用されるため、イメージにそのパスの内容がなければ同じ理由で失敗することがあります。バインドマウントではホストのディレクトリがそのまま表示されます。Docker がそのディレクトリを作成した場合、root 所有になります。docker compose exec <service> id を実行してコンテナが使用する数値 ID を確認し、ホストのディレクトリを sudo chown -R <uid>:<gid> します。または、daemon が user namespace を remap していない場合は、サービスで user: "1000:1000" を設定します。daemon が user-namespace remapping を使用する場合は、コンテナの ID がホスト上で移動されるため、代わりにマッピング後のホスト ID に chown します。user: ではこの移動は変わりません。
Docker は名前付きボリュームをディスク上のどこに保存しますか?
デフォルトの local driver では /var/lib/docker/volumes/<volume>/_data の下に保存され、docker volume inspect <volume> が正確な Mountpoint を表示します。確認が必要な場合は読み取れますが、書き込みはコンテナ経由だけで行ってください。ホスト上で root として編集すると、コンテナが想定しない形で所有権が変わるためです。
名前付きボリュームをバックアップするにはどうすればよいですか?
ボリュームとホストのディレクトリを両方マウントした短期間のコンテナを実行し、docker run --rm -v myvol:/data:ro -v "$PWD":/backup alpine tar czf /backup/myvol.tar.gz -C /data . で一方から他方へアーカイブします。データベースの場合は、稼働中のファイルをコピーするのではなく、データベース固有のツールで dump を取得してください。書き込み中に取得したファイルコピーは、破損した状態に復元される可能性があるためです。
docker compose down はボリュームを削除しますか?
docker compose down はコンテナとネットワークを削除しますが、名前付きボリュームは残します。docker compose down -v はプロジェクトが宣言するすべての名前付きボリュームも完全に削除します。バインドマウントはどちらのコマンドでも削除されません。そのディレクトリは Docker ではなくホストに属するためです。