Dockerマウントディレクトリの権限問題完全解決ガイド:診断から実践まで5つの解決策
ターミナルに表示された、赤いエラーメッセージ「Permission denied」。本日これで5回目になります。Macでは問題なく動作していた開発用コンテナが、Linuxの本番サーバーにデプロイした途端に動きません。コンテナが生成したログファイルを削除しようとしても、「操作権限がありません」と拒否されます。サーバー管理者であるにもかかわらず、なぜ操作できないのでしょうか。
昨日、同僚が「chmod 777にすれば動くよ」と軽く言いました。試してみると、確かに解決します。しかし、「本当にこのままで大丈夫だろうか」と、心の中で不安がよぎります。
Dockerコミュニティフォーラムの統計によると、初心者の40%がマウントディレクトリの権限問題に直面し、そのうちの60%がchmod 777を選択しています。しかしその結果、コンテナエスケープやデータ漏洩といった隠れたセキュリティリスクを抱えることになります。
この記事では、Dockerにおける権限問題の本質である「UIDとGIDの仕組み」を徹底的に解説します。その上で、手軽な一時しのぎのハックからエンタープライズレベルの安全な設計まで、5つの適切な解決策を提示します。さらに、問題を即座に特定するための3つの診断コマンドも紹介します。
根本原因:なぜ権限問題が起きるのか
UID/GIDこそが真の身分証明書
Linuxはユーザー名で身元を識別していると思っていませんか?間違いです。Linuxカーネルは数字しか見ていません。それがUID(ユーザーID)とGID(グループID)です。ユーザー名は人間が見るためのニックネームに過ぎません。
例えば、あなたのPCで id コマンドを実行してみてください:
uid=1000(oden) gid=1000(oden) groups=1000(oden)
見えましたか?1000こそがあなたの真の身分証明です。「oden」という名前はカーネルにとって重要ではありません。
次にrootユーザーを見てみましょう:
uid=0(root) gid=0(root) groups=0(root)
ID 0のユーザーがスーパーユーザーです。名前が何であれ、UIDが0であれば、システムの最高権限を持ちます。
権限の衝突はどこから来るのか
ここが問題の核心です。ホストマシン上で一般ユーザー(例:UID=1000)としてDockerを実行しているのに、コンテナがデフォルトでroot(UID=0)として実行されると、矛盾が生じます。
完全な衝突のプロセスは以下の通りです:
- Linuxホスト上で、UID=1000の一般ユーザーとしてコンテナを起動する。
- コンテナ内のプロセスはデフォルトでroot(UID=0)として実行される。
- コンテナ内のrootがファイルを作成する(例:
/app/logs/output.log)。 - このファイルはbind mountを通じてホストの
./logs/output.logにマッピングされる。 - ホスト上でこのファイルの所有者を見るとroot(UID=0)になっている。
- あなたは一般ユーザー(UID=1000)としてこれを削除しようとするが、権限不足で拒否される。
非常に単純明快です。コンテナはあなたがホスト上で誰なのかを知りません。知っているのはUIDだけです。ID 0が作ったファイルを、非0ユーザーが触れるわけがありません。
なぜMacやWindowsでは問題が起きないのか?
「あれ?MacでDockerを使っていてこんな問題起きたことないけど?」と思うかもしれません。
そうです。MacとWindows上のDocker Desktopは仮想マシン内で動作しているからです。MacはApple Virtualizationフレームワーク(以前はhyperkit)、WindowsはWSL2またはHyper-Vを使用しています。これらには追加の「権限変換レイヤー」が存在します。
MacのVirtioFSファイルシステムは、コンテナが生成したファイルの所有者を自動的にホストの現在のユーザーに変換してくれます。便利ですよね?しかし、これこそが「Macでは動くのにLinuxサーバーでは動かない」という現象の根本原因です。Linux上のDockerはカーネルを直接呼び出すため、この中間変換レイヤーがないのです。
簡単に言えば、Docker Desktopはユーザー体験のために妥協し、いくつかの「真実性」を犠牲にしました。開発段階では痛みを感じませんが、デプロイ時に呆然とすることになります。
その他の注意点
Bind mount vs Named Volume:
- Bind mount(
-v /host/path:/container/path)はホストディレクトリを直接マッピングするため、権限問題が最も顕著です。 - Named Volume(
-v mydata:/container/path)はDockerによって管理されるため、権限は比較的緩やかですが、問題がないわけではありません。
SELinuxとAppArmor:
もしLinuxでSELinux(CentOS/RHEL)やAppArmor(Ubuntu)が有効になっている場合、権限問題はさらに複雑になります。UID/GIDの一致だけでなく、セキュリティコンテキストラベルも考慮する必要があります。原因不明の権限エラーに遭遇したら、まずSELinuxのログを確認してください:
sudo ausearch -m avc -ts recent
コンテナ内にユーザーが存在しない:
コンテナイメージにはデフォルトでrootと少数のシステムユーザーしかいません。ホスト上のUID=1000のユーザーなんて、コンテナは知りません。ファイルの所有者が数字の羅列で表示されるのはそのためです。
スピード診断:権限問題を特定する3つのコマンド
Permission deniedに遭遇しても慌てないでください。プロはどうやって問題を特定するのでしょうか?3つのコマンドで、1分で終わります。
コマンド1:ファイルの本当の所有者を確認する
ls -ln /your/mount/path
注意:-l ではなく -ln です。違いは? -l はユーザー名を表示し、-ln はUID/GIDの数字を表示します。
出力例:
-rw-r--r-- 1 0 0 1024 Dec 17 10:00 output.log
読み方:
- 1列目
-rw-r--r--は権限ビット(重要ではありません) - 3列目
0が所有者のUID ← ここが重要 - 4列目
0が所有者のGID ← ここも - その後はファイルサイズ、時間、ファイル名
0 0 が見えましたか?これがrootユーザーです。もしホスト上のあなたのUIDが1000なら、当然このファイルを変更することはできません。
正常な場合と比較してみましょう:
ls -ln ~/my-project
出力:
-rw-r--r-- 1 1000 1000 2048 Dec 17 11:30 README.md
1000 1000 ですね。これなら自分のファイルです。
コマンド2:コンテナプロセスの実際のIDを確認する
docker exec <container_name> id
出力例:
uid=0(root) gid=0(root) groups=0(root)
これはコンテナ内のプロセスがどのIDで実行されているかを教えてくれます。通常はroot(UID=0)です。
ホスト側と比較してみましょう:
id
出力:
uid=1000(oden) gid=1000(oden) groups=1000(oden)...
違いがわかりましたか?コンテナ内は0、ホストは1000。不一致です。これが衝突の原因です。
コマンド3:Dockerのマウント設定を確認する
docker inspect <container_name> | grep -A 10 "Mounts"
出力例:
"Mounts": [
{
"Type": "bind",
"Source": "/home/oden/project/logs",
"Destination": "/app/logs",
"Mode": "",
"RW": true,
"Propagation": "rprivate"
}
]
見るべきポイント:
Type:bindかvolumeか?bind mountの方が権限問題が起きやすい。Source:ホストパス。ls -lnでこのパスの所有者を確認。RW:true なら読み書き可、false なら読み取り専用Mode:SELinux 用の:z/:Zなどのマウントオプションがあるか
1分診断フロー
権限問題に遭遇したら、この順序でチェック:
- まずファイルを見る:
ls -lnで問題のファイルのUID/GIDを確認 - 次にコンテナを見る:
docker exec <container> idでコンテナプロセスの身元を確認 - 差異を比較:コンテナUIDとファイルの所有者UIDが、あなたのホストUIDと異なるなら、権限衝突です
- 設定を確認:
docker inspectでマウント方式とパスを確認
実際の例:コンテナログが削除できない場合
# 手順1:ファイル所有者を確認
$ ls -ln ./logs/
-rw-r--r-- 1 0 0 ... app.log
# UID=0、rootが作成
# 手順2:コンテナ身分を確認
$ docker exec myapp id
uid=0(root) ...
# コンテナはrootで実行中
# 手順3:自分の身分を確認
$ id
uid=1000 ...
# 私は1000。コンテナは0。不一致!
診断結果:コンテナがrootで実行され、root所有のファイルを生成したため、私は削除権限がない。
この診断ができれば、どの解決策を使うべきかわかります。
5大解決策:あなたに最適なのはどれ?
原因と診断方法がわかったところで、解決策を見ていきましょう。簡単なものから複雑なものまで、5つの方法を紹介します。重要なのは、状況に応じて適切な方法を選ぶことです。
解決策1:実行時に—userパラメータでUID/GIDを指定する
対象:素早いテスト、またはローカル開発環境
原理:Dockerに「私のUIDでコンテナを実行して」と指示します。これでコンテナが生成するファイルの所有者はあなたになります。
使い方:
# コマンドライン方式
docker run --user $(id -u):$(id -g) -v /host/data:/app/data myimage
# docker-compose.yml方式
services:
myapp:
image: myimage
user: "${UID:-1000}:${GID:-1000}"
volumes:
- ./data:/app/data
実行時:
export UID=$(id -u)
export GID=$(id -g)
docker-compose up
メリット:
- 最も簡単、即効性あり
- Dockerfileの変更やイメージ再ビルドが不要
- ローカル開発の高速イテレーションに最適
デメリット:
- 起動のたびに指定が必要
- コンテナ内アプリが特定のUIDを必要とする場合(例:nginxが80ポートバインドにroot権限を要する場合)は失敗する
- チームメンバーのUIDが異なると面倒(環境変数でカバー可)
リスク:低
適用システム:Linuxは完璧に動作。Mac/Windowsは対応しているが体験は微妙(VM層のため)。
いつ使う?:ローカル開発、一時的なテスト、アイデアの検証。Macで開発していて、Linux CIにプッシュしたら権限エラーが出たときの応急処置として。
解決策2:Dockerfileでマッチするユーザーを作成する
対象:チーム共有のイメージ、再利用が必要なシーン
原理:イメージビルド時にbuild argでホストのUIDを渡し、イメージ内で対応するユーザーを作成します。コンテナ起動後はそのユーザーで実行されます。
使い方:
Dockerfile:
FROM python:3.11
# ビルド引数を受け取る
ARG UID=1000
ARG GID=1000
# ユーザーグループとユーザーを作成
RUN groupadd -g $GID appuser && \
useradd -m -u $UID -g $GID appuser
# 作業ディレクトリ設定と権限付与
WORKDIR /app
RUN chown -R appuser:appuser /app
# 非rootユーザーに切り替え
USER appuser
# 以降のコマンドはappuser権限で実行
COPY --chown=appuser:appuser . /app
RUN pip install -r requirements.txt
CMD ["python", "app.py"]
ビルド:
docker build --build-arg UID=$(id -u) --build-arg GID=$(id -g) -t myapp:latest .
docker-compose.yml:
services:
myapp:
build:
context: .
args:
UID: ${UID:-1000}
GID: ${GID:-1000}
volumes:
- ./data:/app/data
メリット:
- 一度ビルドすれば、何度実行しても正しい権限
- コンテナ内に完全なユーザー環境がある(ホームディレクトリ、シェル設定など)
- 最もプロフェッショナルな、本番レベルの解決策
デメリット:
- Dockerfileの修正が必要
- チームメンバーのUIDが異なると、各自でビルドが必要(イメージ共有が難しい)
- アプリ起動時にroot権限が必要な場合は使えない
リスク:低
適用システム:Linuxは完璧。Mac/WindowsもVM層があるが使用可能。
いつ使う?:チームで標準ベースイメージがあり、全プロジェクトがそれに基づいている場合。または、ユーザーに配布するイメージ(OSSなど)で、ユーザー自身にビルドさせてUIDを合わせる場合。
解決策3:Entrypointスクリプトで動的調整(gosu方式)
対象:アプリがrootで初期化し、その後降格して実行する必要がある場合
原理:コンテナ起動時はrootでentrypointスクリプトを実行し、スクリプト内で動的にユーザーを作成し、gosu(su/sudoより安全なツール)を使ってターゲットユーザーに切り替えてメインプログラムを実行します。
使い方:
Dockerfile:
FROM node:18
# gosuのインストール
RUN apt-get update && apt-get install -y gosu && rm -rf /var/lib/apt/lists/*
# entrypointスクリプトをコピー
COPY entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/entrypoint.sh
WORKDIR /app
COPY . /app
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["node", "server.js"]
entrypoint.sh:
#!/bin/bash
set -e
# LOCAL_USER_ID環境変数が指定された場合
if [ -n "$LOCAL_USER_ID" ]; then
# ユーザー作成(存在しなければ)
useradd -u $LOCAL_USER_ID -o -m appuser 2>/dev/null || true
# /appディレクトリの所有者を変更
chown -R appuser:appuser /app
# gosuを使ってappuserで後続コマンドを実行
exec gosu appuser "$@"
else
# 指定がなければrootで実行
exec "$@"
fi
実行:
docker run -e LOCAL_USER_ID=$(id -u) -v ./data:/app/data myapp
メリット:
- 柔軟性が最高:rootでの初期化と一般ユーザーでの実行を両立
- イメージを異なるUIDのユーザー間で共有可能
- 安全性が高い(gosuへの信頼)
デメリット:
- Dockerfileとentrypointの修正が必要
- 複雑さとメンテナンスコストが増加
- gosuのインストールが必要(サイズは小さいが)
リスク:中(gosuはDocker公式推奨ツール)
適用システム:全システム
いつ使う?:アプリ起動時にシステム設定変更(root必要)が必要だが、実行時は一般ユーザーであるべき場合。例:nginxの80ポートバインド、DBのスキーマ初期化など。
解決策4:User Namespace Remapping(userns-remap)
対象:会社のセキュリティ規定で強制隔離が必要、いかなるコンテナも真のrootで実行してはならない場合
原理:Dockerデーモンレベルの設定で、全コンテナのUIDを自動的に「サブユーザー」範囲にリマップします。コンテナ内では自分がroot(UID=0)だと思っていますが、ホスト上では実際には一般ユーザー(例:UID=100000)です。
使い方:
/etc/docker/daemon.json を編集:
{
"userns-remap": "default"
}
Docker再起動:
sudo systemctl restart docker
Dockerは自動的に dockremap ユーザーを作成し、/etc/subuid と /etc/subgid にUID/GID範囲を割り当てます。
検証:
# コンテナ起動
docker run -d --name test -v /tmp/test:/data busybox sleep 3600
# コンテナ内ではrootに見える
docker exec test id
# uid=0(root) gid=0(root)
# しかしホスト上では
ls -ln /tmp/test
# 所有者は大きな数字(例:100000)
メリット:
- 一度の設定で全コンテナに適用
- 全コンテナが自動的に隔離され、イメージやコマンドの変更不要
- 安全性が最高:コンテナエスケープしても、サブユーザーシェルに出るだけで真のrootは取れない
- Docker公式推奨のエンタープライズソリューション
デメリット:
- システムレベルの設定で、全コンテナに影響する
- 既存のコンテナやボリュームが非互換になる可能性(再作成が必要)
- Rootlessモードと同時には使えない
- 一部の特権操作(mountなど)が制限される
リスク:低(公式推奨)
適用システム:Linuxのみ(カーネルのuser namespaceサポートが必要)
いつ使う?:セキュリティ規定が厳しい;マルチテナント環境で、特定のコンテナイメージを信頼できない;プロジェクトごとに設定するのが面倒で、一括で解決したい場合。
解決策5:Rootless Docker
対象:最高レベルのセキュリティ要件、多少の機能制限は許容できる場合
原理:Dockerデーモン自体を非rootユーザーで実行します。全コンテナはこのユーザーのnamespace内で動作し、システムrootから完全に隔離されます。
使い方:
Rootless Dockerのインストール:
# ルートDockerをアンインストール(あれば)
sudo apt-get remove docker docker-engine docker.io
# Rootless Dockerインストール
curl -fsSL https://get.docker.com/rootless | sh
# 環境変数設定
export PATH=$HOME/bin:$PATH
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock
# 起動
systemctl --user start docker
systemctl --user enable docker
検証:
docker run hello-world
# 完全に非rootとして実行される
メリット:
- 究極のセキュリティ:デーモンもコンテナもrootではない
- コンテナエスケープしても、あなたのユーザー権限範囲から出られない
- 信頼できないイメージ、マルチテナント、セキュリティに敏感な環境に最適
デメリット:
- 特権ポート(1024以下、80/443など)が使えない
- 一部のネットワークモード(hostモードなど)が使えない
- パフォーマンスがやや落ちる(追加のnamespaceオーバーヘッド)
- 設定が比較的複雑、ドキュメントが少なめ
リスク:低(設計通り、公式サポート)
適用システム:モダンLinux(newuidmap/newgidmapサポートが必要、Ubuntu 20.04+、CentOS 8+)
いつ使う?:金融・医療など極めて厳しいセキュリティ規定がある;信頼できないサードパーティイメージを実行する;K8sクラスタで全Podの非root実行が求められている。
クイック決定:どれを使うべき?
5つの解決策、まだ迷いますか?この決定木を使ってください:
権限問題に遭遇?
├─ とりあえず一時的にテストしたいだけ?
│ └─ Yes → 解決策1(--userパラメータ)
│
├─ チームで長期メンテするプロジェクト?
│ ├─ アプリ起動にroot権限が必要?
│ │ └─ Yes → 解決策3(entrypoint+gosu)
│ └─ root権限不要?
│ └─ 解決策2(Dockerfileでユーザー作成)
│
├─ 会社のセキュリティ規定で強制隔離が必要?
│ ├─ 特権ポートや特殊機能が必要?
│ │ └─ Yes → 解決策4(userns-remap)
│ └─ 特権不要?
│ └─ 解決策5(Rootless Docker)
│
└─ ローカル開発問題をサクッと解決したい?
└─ 解決策1(--userパラメータ)
私のアドバイス:
- 開発環境:解決策1(早くて効果的)
- チームプロジェクト:解決策2または3(プロフェッショナルで標準的)
- 本番環境:解決策4または5(安全第一)
複雑な解決策を急がないでください。要件に応じて選びましょう。十分であればそれがベストです。
クロスプラットフォームの特殊性:Mac・Windows・Linux の違い
「うちのマシンでは問題ないんだけど」
このフレーズ、聞き覚えがありませんか。Mac で開発すると問題なく動くのに、Linux サーバーに載せたら壊れる。逆に Linux では平気なのに、Windows の開発 PC では妙な挙動が出る——原因は、3 つの OS で Docker の実装が大きく違うことにあります。
Linux:いちばん「本番に近い」が、問題も出やすい
Linux 上の Docker は仮想マシンの中間層なしでカーネルを直接使います。本番環境に最も近い一方、権限問題もいちばん表に出ます。
特徴:
- コンテナとホストが同じカーネルを共有する
- UID/GID がそのままマッピングされ、変換レイヤーがない
- デフォルトでコンテナは root(UID=0)として動く
- bind mount の権限衝突がそのまま表面化する
ベストプラクティス:
- 開発中は解決策 1(
--user)で素早く対処する - 長期プロジェクトは解決策 2(Dockerfile でユーザー作成)
- 本番は解決策 4 または 5(userns-remap / rootless)
よくある落とし穴:
# コンテナが作ったファイルが削除できない
rm: cannot remove 'logs/app.log': Permission denied
# 所有者を確認
ls -ln logs/
# -rw-r--r-- 1 0 0 ...
# 原因:コンテナが root で動き、root 所有のファイルができる
対処:docker-compose.yml に user: "${UID}:${GID}" を追加する。
Mac:権限は「ゆるい」が、罠もある
Mac の Docker Desktop は軽量 VM(Apple Virtualization フレームワーク)上で動き、VirtioFS がファイル所有者を自動変換します。
特徴:
- コンテナが作ったファイルの owner が、多くの場合ホストの現在ユーザーになる
- 開発中は権限問題をほとんど感じない
- その「便利さ」が、デプロイ時に裏目を出す
既知の問題:
- VirtioFS は 2023〜2024 年に権限関連の不具合が複数(ネストしたディレクトリの権限乱れなど)
- Docker Desktop 4.13 以降で多くは修正済みだが、エッジケースは残る
- シンボリックリンクを多段で跨ぐと権限が失われることがある
ベストプラクティス:
- ローカル開発はそのまま使ってよいが、便利さに依存しない
- Dockerfile では解決策 2 でユーザーを作る
- デプロイ前に Linux(または VM)で一度テストする
よくある罠:
# Mac ではこれで問題ない
services:
app:
image: myapp
volumes:
- ./data:/app/data
# コンテナは root だが、ファイル owner は自動であなたになる
# Linux にデプロイすると壊れる
# ファイルがすべて root になり、CI スクリプトが触れない
対処:Mac で問題がなくても user を付ける:
services:
app:
user: "${UID:-1000}:${GID:-1000}"
Windows:いちばん複雑な環境
Windows の Docker Desktop は WSL2 または Hyper-V 上で動き、NTFS の権限モデルと Linux の ACL は別物です。
特徴:
- WSL2 モード:Linux に近いが、NTFS と ext4 を跨ぐと変換が入る
- Hyper-V モード:仮想化が一段増え、権限変換も複雑
- BitLocker で暗号化されたドライブでは、さらに挙動が不安定になりやすい
よくある問題:
# C ドライブへ bind mount
docker run -v C:\Users\oden\project:/app myimage
# 権限が乱れ、読めるが書けない、など
# WSL パスへ bind mount
docker run -v /mnt/c/Users/oden/project:/app myimage
# マシはするが、まだ問題が残ることも
ベストプラクティス:
- bind mount より Named Volume を優先する:
services: db: image: postgres volumes: - pgdata:/var/lib/postgresql/data volumes: pgdata: - bind mount が必要なら、プロジェクトは WSL2 のファイルシステム内(
\\wsl$\Ubuntu\home\...)に置く - ドライブを跨いだマウントは避ける
既知の問題:
- NTFS 上ではパーミッションが 777 に見えることがある(見た目は怖いが、実際は NTFS が制御)
- Windows ではシンボリックリンクの扱いが限定的で、コンテナから見えないことがある
- 改行(LF / CRLF)が Git と Docker の両方で混ざりやすい
クロスプラットフォームチームの統一戦略
Mac・Linux・Windows が混在するチームでは、次のように揃えます。
推奨設定:
docker-compose.yml:
services:
app:
build:
context: .
args:
UID: ${UID:-1000}
GID: ${GID:-1000}
user: "${UID:-1000}:${GID:-1000}"
volumes:
- ./src:/app/src
Dockerfile:
FROM node:18
ARG UID=1000
ARG GID=1000
RUN groupadd -g $GID appuser && \
useradd -m -u $UID -g $GID appuser
WORKDIR /app
RUN chown appuser:appuser /app
USER appuser
.env.example(チーム共有):
# Linux/Mac
# export UID=$(id -u)
# export GID=$(id -g)
# Windows は固定値でも可
UID=1000
GID=1000
README.md の例:
## プロジェクトの起動
**Linux/Mac**:
```bash
export UID=$(id -u) GID=$(id -g)
docker-compose up
Windows:
# WSL2 内で実行、またはデフォルト 1000 のまま docker-compose up
docker-compose up
**ポイント**:
- build args と環境変数で柔軟にする
- Linux は実際の UID、Mac/Windows はデフォルト値
- Dockerfile でユーザーを作り、イメージを全 OS で揃える
- README で OS ごとの差を明示する
### ひとことまとめ
- **Linux**:問題が最も目立つが、解決策も豊富。本番に最も近い
- **Mac**:たいていは問題なく見えるが、便利さに油断しない。設定はきちんと
- **Windows**:volume 優先、プロジェクトは WSL2 内に置く
クロスプラットフォームのチームなら、build args と `user` で全員が同じように動かせるようにする。
## 実践ケース:よくある 5 シーンの解き方
原理・診断・解決策・クロスプラットフォームの違いまで説明してきました。ここからは、いちばん多い 5 つの権限トラブルを、手順付きで解きます。
### ケース 1:ローカル開発でコンテナログが削除できない
**症状**:
アプリコンテナがログを吐き、しばらくして整理しようとすると:
```bash
rm -rf logs/
# rm: cannot remove 'logs/app.log': Permission denied
診断手順:
# 1. ファイルの owner
$ ls -ln logs/
total 1024
-rw-r--r-- 1 0 0 524288 Dec 17 14:30 app.log
-rw-r--r-- 1 0 0 524288 Dec 17 14:31 error.log
# UID=0 → root が作成
# 2. コンテナの ID
$ docker exec myapp id
uid=0(root) gid=0(root) groups=0(root)
# 3. 自分の ID
$ id
uid=1000(oden) gid=1000(oden) groups=1000(oden)
# 自分は 1000、コンテナは 0 → 不一致
解決策:
docker-compose.yml に user を追加:
services:
myapp:
image: myapp:latest
user: "${UID:-1000}:${GID:-1000}"
volumes:
- ./logs:/app/logs
実行:
export UID=$(id -u)
export GID=$(id -g)
docker-compose down
docker-compose up
コンテナはあなたの UID で動き、ログの owner もあなたになります。
ひとこと:user を 1 行足すだけ。
ケース 2:Django/Flask の静的ファイル権限
症状:
collectstatic のあと:
docker exec webapp python manage.py collectstatic
ls -ln static/
# drwxr-xr-x 1 0 0 ...
# owner が root。CI や nginx コンテナが読めない
原因:
コンテナが root で動き、生成物も root 所有。後段の nginx が別 UID だと読めない。
解決策:
Dockerfile でアプリユーザーを作成:
FROM python:3.11
RUN groupadd -g 1000 appuser && \
useradd -m -u 1000 -g 1000 appuser
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY --chown=appuser:appuser . /app
USER appuser
CMD ["gunicorn", "myapp.wsgi:application"]
docker-compose.yml:
services:
webapp:
build: .
volumes:
- static_volume:/app/static
nginx:
image: nginx:alpine
volumes:
- static_volume:/usr/share/nginx/html/static:ro
ports:
- "80:80"
volumes:
static_volume:
ポイント:
- Dockerfile で UID=1000 のユーザーを作る
- 静的ファイルは Named Volume で共有(bind mount より安全)
- nginx は自分のユーザーで volume を読む(Docker が権限を調整)
ひとこと:ユーザー作成 + volume 共有。
ケース 3:データベースボリュームの権限
症状:
docker-compose up postgres
# postgres: could not open file "/var/lib/postgresql/data/...": Permission denied
原因:
DB イメージは特定 UID(postgres なら 999 など)に切り替えて動く。bind mount のホスト側 owner が合わないと起動に失敗する。
診断:
ls -ln ./pgdata
# drwxr-xr-x 1 1000 1000 ...
# owner は 1000 だが、postgres は 999 を要求
docker run --rm postgres:15 id
# uid=999(postgres) gid=999(postgres)
解決策:
方法 A:Named Volume(推奨)
services:
postgres:
image: postgres:15
environment:
POSTGRES_PASSWORD: secret
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
方法 B:bind mount が必須なら事前に chown
mkdir -p ./pgdata
sudo chown -R 999:999 ./pgdata
services:
postgres:
image: postgres:15
volumes:
- ./pgdata:/var/lib/postgresql/data
注意:イメージごとに UID は異なる(PostgreSQL/MySQL/MongoDB/Redis は多くが 999 だが、バージョンで変わることもある)。docker run --rm <image> id で確認する。
ひとこと:DB は Named Volume。bind mount なら事前 chown。
ケース 4:CI でビルド成果物の権限がずれる
症状:
# .gitlab-ci.yml
build:
script:
- docker run --rm -v $CI_PROJECT_DIR:/app builder npm run build
- ls -l dist/
# owner が root
- cp dist/* /deploy/
# Permission denied
CI runner は一般ユーザーだが、コンテナは root でビルドするため、後続ステップが触れない。
解決策:
方法 A:ビルド後に owner を変更
FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build && \
chown -R 1000:1000 /app/dist
CMD ["npm", "run", "build"]
方法 B:--user でビルド
build:
script:
- docker run --rm --user $(id -u):$(id -g) -v $CI_PROJECT_DIR:/app builder npm run build
- cp dist/* /deploy/
方法 C:entrypoint で柔軟に
FROM node:18
RUN apt-get update && apt-get install -y gosu
COPY entrypoint.sh /
RUN chmod +x /entrypoint.sh
WORKDIR /app
ENTRYPOINT ["/entrypoint.sh"]
CMD ["npm", "run", "build"]
entrypoint.sh:
#!/bin/bash
set -e
npm run build
if [ -n "$OUTPUT_UID" ]; then
chown -R $OUTPUT_UID:${OUTPUT_GID:-$OUTPUT_UID} /app/dist
fi
CI:
build:
script:
- docker run --rm -e OUTPUT_UID=$(id -u) -v $CI_PROJECT_DIR:/app builder
ひとこと:成果物の owner を明示するか、--user でビルドする。
ケース 5:Kubernetes Pod の権限
症状:
kubectl logs mypod
# Error: EACCES: permission denied, open '/app/data/config.json'
原因:
securityContext の実行ユーザーと、volume 内ファイルの owner / fsGroup が合っていない。
診断:
kubectl exec -it mypod -- id
# uid=1000 gid=1000
kubectl exec -it mypod -- ls -ln /app/data
# drwxr-xr-x 2 0 0 ...
# owner は root、Pod は 1000 で動いている
解決策:
Pod spec に securityContext を設定:
apiVersion: v1
kind: Pod
metadata:
name: mypod
spec:
securityContext:
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
containers:
- name: app
image: myapp:latest
volumeMounts:
- name: data
mountPath: /app/data
volumes:
- name: data
emptyDir: {}
ポイント:
runAsUser:プロセスの UIDrunAsGroup:プロセスの GIDfsGroup:volume 内ファイルのグループと読み書き権
PersistentVolumeClaim の例:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mypvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: Pod
metadata:
name: mypod
spec:
securityContext:
fsGroup: 1000
containers:
- name: app
image: myapp:latest
securityContext:
runAsUser: 1000
volumeMounts:
- name: storage
mountPath: /app/data
volumes:
- name: storage
persistentVolumeClaim:
claimName: mypvc
ひとこと:runAsUser と fsGroup を明示する。
5 ケースのまとめ
| シーン | 症状 | 解決の方向 | おすすめ |
|---|---|---|---|
| ローカル開発のログ | Permission denied | user 設定 | docker-compose に user |
| 静的ファイル収集 | nginx が読めない | Dockerfile でユーザー | USER appuser + volume |
| DB 起動失敗 | データディレクトリに書けない | Named Volume | Docker に任せる |
| CI 成果物 | 後続ステップが触れない | --user / chown | ビルド時に owner 指定 |
| K8s Pod | EACCES | securityContext | runAsUser + fsGroup |
シーンごとに処方が違います。まず診断し、衝突の種類を特定してから手を打ちましょう。
結論
冒頭の「深夜 3 時、Permission denied を見つめていた」場面を思い出してください。管理者なのにファイルが消せない——その理由が、もう分かります。
根本原因:Linux は UID/GID しか見ない。コンテナ内 root(0)が作ったファイルを、ホストの一般ユーザー(1000)は触れない。
診断:ls -ln で owner、docker exec <container> id でコンテナ、docker inspect でマウント——3 コマンドで 1 分。
解決策(5 つから選択):
--user:手早い。ローカル検証向き- Dockerfile でユーザー作成:チームの定番
- entrypoint + gosu:root で初期化し、実行は降格
- userns-remap:企業向けの強制隔離
- Rootless Docker:最高の安全性(機能制限あり)
クロスプラットフォーム:Mac/Windows の Docker Desktop は権限変換層があり、問題が見えにくい。Linux はカーネル直結で衝突がそのまま出る。Mac の便利さに甘えず、Dockerfile で正しく設定する。
実践:上記 5 ケースが、開発・静的ファイル・DB・CI・K8s の典型パターンです。
今日からできること
今日(5 分):
docker-compose.ymlにuser: "${UID:-1000}:${GID:-1000}"を足すdocker exec <container> idでコンテナの実 UID を確認するls -lnで権限問題を 1 件診断してみる
今週(1〜2 時間):
- Dockerfile に
ARG UID/GIDとユーザー作成を入れる - 診断コマンドをチーム wiki や README に載せる
- 役に立てば同僚に共有する
継続的に:
- 社内規程があれば userns-remap / rootless を検討する
- 本番の Docker 設定を見直し、権限隔離を確認する
- CI/CD に権限チェックを組み込む
最後に
権限問題は難しく見えますが、本質は「身元の照合」です。コンテナはホスト上のあなたの名前を知りません。数字だけです。
UID/GID の対応が分かれば、chmod 777 に頼らず、深夜に Permission denied で悩まなくて済みます。
適切な手を選び、コマンドを当て、理由まで押さえれば十分です。
9分で読めます · 公開日: 2025年12月17日 · 更新日: 2026年6月8日
Docker 実践ガイド
検索からこのページに来た場合は、前後の記事もあわせて読むと同じテーマの理解がかなり早く深まります。
前の記事
Docker マウント方式比較:Volume vs Bind Mount 選択ガイド(パフォーマンステスト付き)
Docker の 3 つのマウント方式を深掘り比較。Mac で npm install が 3 倍遅くなる問題の原因と対策、判断ツリーと実践シナリオで Volume・Bind Mount・tmpfs を素早く選べるように解説します。
第 15 / 37 記事
次の記事
Dockerネットワークモード完全解説:bridge/host/none/containerの4モード性能比較と使い分け
Dockerの4つのネットワークモード(bridge/host/none/container)の仕組み、性能比較、適用シナリオを深掘り解説。正しいネットワーク設定の選び方を実践例と意思決定ガイド付きで紹介します。
第 17 / 37 記事
関連記事
Dockerfile入門:ゼロから最初の Docker イメージを作る(実例付き)
Dockerfile入門:ゼロから最初の Docker イメージを作る(実例付き)
Docker vs 仮想マシン:5分で理解する性能差とシーン別選び方ガイド
Docker vs 仮想マシン:5分で理解する性能差とシーン別選び方ガイド
Docker インストールの落とし穴ガイド 2025:permission denied から正常起動までの完全解決策
コメント
GitHubアカウントでログインしてコメントできます