bind mount と UID/GID 問題
ホストとコンテナの UID/GID がずれて「コンテナが作ったファイルがホストから消せない」「Permission denied」に陥る問題を、原因と対処のパターンで整理します。
なぜズレるのか
Linux ではファイルの所有者は UID(数値) で管理されており、名前はあくまで /etc/passwd 経由の飾りです。
ホストの alice(UID 1000)が作ったファイルも、コンテナ内からは「UID 1000 のファイル」としか見えません。
コンテナ内で root(UID 0)として動くプロセスがホストの bind mount 上にファイルを作ると、ホスト側にも UID 0(= root)所有のファイルができます。
これにより、
- ホスト側の一般ユーザーでは削除・書き換えができない
.next/dist/node_modulesのようなビルド成果物が毎回 root 所有になる
という現象が起きます。
rootless Podman や user namespace を使うと緩和されますが、素の Docker + Linux 環境で最もよく遭遇する問題です。
症状あるある
rm -rf node_modulesがPermission deniedになる → コンテナ内 root が作ったため- VS Code で「ファイル保存できません」 → マウント先が root 所有になり、ホストの自分に書き込み権限がない
git statusで変なモードビット変更が出る → Windows / WSL2 でファイルシステムを跨ぐと、実行ビットがばらつくことがある- CI で
COPY . .後にパーミッションが壊れる → ビルドコンテキスト内のファイルの所有者が想定外
macOS / Windows の Docker Desktop では内部で UID マッピングが働くので気づきにくいですが、Linux ホストや WSL2 内で bind mount したときに顕在化します。
対処 1: 非 root ユーザーで動かす + UID を合わせる
Dockerfile で非 root ユーザーを作成し、実行時にホストユーザーの UID/GID に合わせる方法です。
FROM node:20-alpine
ARG UID=1000
ARG GID=1000
RUN addgroup -g ${GID} app && adduser -D -u ${UID} -G app app
WORKDIR /app
USER app
COPY --chown=app:app . .
CMD ["node", "server.js"]
docker build --build-arg UID=$(id -u) --build-arg GID=$(id -g) -t my-app .
docker run --rm -v "$PWD:/app" my-app
ホストユーザーと同じ UID/GID でプロセスを動かすので、bind mount 越しに作られるファイルもホストから自然に扱えます。
対処 2: --user フラグで上書きする
Dockerfile をいじれない場合(公式イメージを使っている等)は、docker run --user で実行時に UID/GID を指定できます。
docker run --rm -u "$(id -u):$(id -g)" -v "$PWD:/app" -w /app node:20 node server.js
注意点:
- 指定した UID がイメージ内の
/etc/passwdに無くても動作するが、一部アプリはwhoamiで「UID xxxx」と表示されて混乱する - UID が
HOMEを持たない場合、npmなど一部ツールがエラーになる。-e HOME=/tmpを足すか、書き込み可能なHOMEを用意する - ポート 80/443 など 1024 未満を bind する用途には使えない(非 root 権限では通常不可)
対処 3: 名前付きボリュームで分離する
ホストと UID を合わせる必要がないデータ(node_modules、キャッシュ、ビルド成果物)は、bind mount から外して 名前付きボリュームに分けるのが手堅いです。
services:
web:
build: .
volumes:
- ./:/app # ソースは bind mount(編集が即反映)
- node_modules:/app/node_modules # 名前付きボリュームで分離
volumes:
node_modules:
こうすれば、node_modules はコンテナ内の UID で書かれてもホストからは見えないので権限問題が起きません。
ソース側は UID 合わせの対策と併用します。
rootless / user namespace という根本解決
Podman の rootless モードや Docker の userns-remap を有効にすると、コンテナ内の UID 0 がホストの UID 0 にマッピングされないため、この問題が構造的に解消されます。
- Podman rootless: 追加設定なしで既定動作。コンテナ内の root はホストの自分に、その他の UID は
subuid/subgidの範囲にマップされる - Docker userns-remap:
/etc/docker/daemon.jsonに"userns-remap": "default"を書いてデーモン再起動
ただし、この設定下では 既存の bind mount の所有権調整が必要になったり、特権を要するコンテナ(Docker-in-Docker 等)が動かなくなるケースがあります。
導入前に CI と開発フローで一通り動作確認をしてください。
