docker『Bind Mount Failed』の原因と対処法

docker『Bind Mount Failed』の原因と対処法

ホストのパスをコンテナへバインドマウントする際に失敗する問題。源流は「ソースパスの不存在」「権限/SELinux/AppArmor」「Docker Desktopのファイル共有未許可」「WSL2/Windowsパスの扱い」「rootless/userns制約」「相対パス・作業ディレクトリの誤認」「ファイル/ディレクトリ種別の不一致」「読み取り専用やnoexecの衝突」など。まずエラーメッセージと実行環境を特定し、パスの正規化→権限とセキュリティラベル→プラットフォーム固有設定→マウント構文の順に修正する。

よく出るエラーメッセージと発生条件

# 典型メッセージ
invalid mount config for type "bind": bind source path does not exist
Error response from daemon: Mounts denied:
bind: permission denied
mkdir /run/desktop/mnt/...: read-only file system

・ホスト側パスが存在しない/参照できない。

・権限不足(UID/GID不一致、SELinux/AppArmor)。

・Docker Desktopで共有外ディレクトリを指定。

・WSL2でWindowsパスの扱いを誤る。

・rootlessモードやusernsでの制約。

・相対パスやComposeのcontext誤認。

・ファイルをディレクトリとして/その逆でマウントしようとして不整合。

クイック診断(5手で切り分け)

# 1) 今の作業ディレクトリと絶対パスを確認
pwd && readlink -f ./hostdir || realpath ./hostdir

# 2) ソースが実在・期待種別か(file/dir)
test -d /path/hostdir && echo "DIR ok"
test -f /path/file.txt && echo "FILE ok"

# 3) 自分のUID/GIDと対象の所有権・モード
id && stat -c '%U:%G %a %n' /path/hostdir

# 4) SELinux/AppArmorの有無
getenforce 2>/dev/null || echo "SELinux not found"

# 5) Docker Desktop/WSL2の場合、共有設定やパス種別を確認
docker info | sed -n '1,20p'

絶対パスで指定(相対パス・カレント誤認を排除)

# NG(相対パス + 実行ディレクトリ依存)
docker run -v ./data:/app/data alpine ls /app/data

# OK(絶対パスで明示)
docker run -v /abs/path/data:/app/data alpine ls /app/data

# --mount 構文(可読性/将来性)
docker run --mount type=bind,source=/abs/path/data,target=/app/data alpine ls /app/data

ソースが“存在していること”を保証(ファイルとディレクトリの違い)

# ディレクトリは存在しないと自動作成されるのは「ターゲット側」
# ソース側は必ず事前に作成する
mkdir -p /abs/path/data

# ファイルをbindする場合、ソースファイルも先に作成
touch /abs/path/config.yml
docker run -v /abs/path/config.yml:/app/config.yml alpine cat /app/config.yml

権限/所有権(UID/GID)の整合

# ホスト側の所有権とモードを最小限で調整
sudo chown -R $USER:$USER /abs/path/data
chmod -R u+rwX,g+rwX /abs/path/data

# コンテナ側をホストのUID/GIDで動かして整合
docker run --user $(id -u):$(id -g) \
  -v /abs/path/data:/app/data alpine sh -lc "id && touch /app/data/ok"

SELinux(RHEL系)なら :z/:Z でラベル付与

# 共有可能ラベル(:z) / 専有ラベル(:Z)
docker run -v /abs/path/data:/app/data:z alpine ls /app/data
docker run -v /abs/path/data:/app/data:Z alpine ls /app/data

# 既存ディレクトリへ手動付与(最終手段)
sudo chcon -Rt svirt_sandbox_file_t /abs/path/data

AppArmor/readonly/noexec の衝突を回避

# 一時的にAppArmorを外す(検証のみ)
docker run --security-opt apparmor=unconfined \
  -v /abs/path/data:/app/data alpine touch /app/data/x

# ターゲットに実行ファイルを置くならnoexecに注意
mount | grep noexec

Docker Desktop(macOS/Windows):ファイル共有の許可

# Settings → Resources → File Sharing に /abs/path/data を追加
# その後、再試行
docker run -v /abs/path/data:/app/data alpine ls /app/data

・macOSで「Mounts denied:」が出る場合は、共有外ディレクトリかフルディスクアクセス未許可が原因。

・Windowsはドライブ単位の共有許可や資格情報の更新が必要なことがある。

WSL2 のパス取り扱い(Linux側かWindows側かを明確化)

# WSL2 Linuxディストリ内のパスを使う(推奨)
docker run -v /home/user/data:/app/data alpine ls /app/data

# Windowsパスをbindする場合は /mnt/c/... を明示(パフォーマンス低下に注意)
docker run -v /mnt/c/Users/user/data:/app/data alpine ls /app/data

・WSL2のDockerはLinuxカーネル側で動作。Windowsの「C:…」表記はそのまま使わず、/mnt/c 経由に統一。

rootless モード/userns の制約

# rootlessかを確認
docker info | grep -i rootless

# rootlessは特定のシステムパス/デバイスのbindに制約
# HOME配下にソースを置く、または必要最小限でrootfulに切り替え

読み取り専用(:ro)やマウントオプションの衝突

# :ro なのに書き込み → 当然失敗
docker run -v /abs/path/data:/app/data:ro alpine sh -lc "echo x > /app/data/a"

# 書き込みが必要なら:rw(デフォルト)
docker run -v /abs/path/data:/app/data:rw alpine sh -lc "echo x > /app/data/a"

Composeの相対パス・context誤認(プロジェクトルート基準)

# docker-compose.yml(相対パスは compose ファイルの位置基準)
services:
  app:
    image: alpine
    volumes:
      - ./data:/app/data        # ← compose.yml の場所から見た ./data

・CLIの実行ディレクトリではなく、composeファイルの場所を基準に解決される点に注意。

・不安なら絶対パスに置き換える。

–mount 構文と -v 構文の差異(混乱を避ける)

# -v は短縮だが曖昧になりやすい
docker run -v /abs/path/data:/app/data alpine

# --mount は明示的でエラー時も読みやすい
docker run --mount type=bind,source=/abs/path/data,target=/app/data alpine

最小再現テンプレ(環境依存要因を切り分け)

# 1) 空の検証用ディレクトリを作る
mkdir -p /abs/path/data && echo ok > /abs/path/data/hello.txt

# 2) readonly/SELinuxなしで単純な確認
docker run --rm -v /abs/path/data:/app/data alpine sh -lc "ls -l /app/data && cat /app/data/hello.txt"

# 3) 書き込み確認
docker run --rm -v /abs/path/data:/app/data alpine sh -lc "echo world >> /app/data/hello.txt && tail -n1 /app/data/hello.txt"

NG→OK 早見表

# NG: 相対パスで失敗 → OK: 絶対パスに
docker run -v ./data:/app/data ...
docker run -v /abs/path/data:/app/data ...

# NG: ソースが存在しない → OK: 事前に作成
mkdir -p /abs/path/data

# NG: ファイル未作成でbind → OK: touchしてからbind
touch /abs/path/app.yml
docker run -v /abs/path/app.yml:/app/app.yml ...

# NG: SELinuxで拒否 → OK: :z/:Z
docker run -v /abs/path/data:/app/data:z ...

# NG: Desktopで共有外 → OK: File Sharingに追加
# NG: WSL2で C:\ を直書き → OK: /mnt/c/... に変換

チェックリスト(上から順に潰す)

1) 絶対パスで指定しているか(--mount推奨)
2) ソースが実在し、期待の種別(file/dir)か
3) 所有権/パーミッションは適切か(UID/GID整合)
4) SELinuxなら :z/:Z、AppArmor・noexecの影響はないか
5) Docker DesktopのFile Sharingに追加済みか
6) WSL2なら /mnt/c を使い、Linux側パスが望ましい
7) rootless/usernsでの制約に該当しないか
8) Composeの相対パスがcompose.yml基準で解決される点を理解しているか