サイトアイコン 協栄情報ブログ

【AWS EC2】コンテナでファイル共有したらPermission denied:バインドマウントとUID/GIDの話

はじめに

EC2(Amazon Linux 2)でコンテナを触り始めて、ホストのファイルをコンテナから編集したくなりました。
昨日は動いて問題ないと思ったのに、翌朝まったく同じ起動で Permission denied
結局、原因は「ユーザー名」じゃなくて“数値のUID/GID”でした。

最初の試行:Volumeマウント

Dockerを学び始めた頃、データを永続化するためにVolumeを使っていました。

docker run -v mydata:/app/data my-image

データは保存されるし、動いている!と思っていました。

問題発生:データは残ってるのに、なぜか書けない

コンテナ再起動後、「データが消えた」ように見えましたが、実際はVolumeにデータは残っていました

ただし、Volumeには以下の問題がありました:

UID/GIDが永続化される: Volume内に作られたファイル/ディレクトリの所有権(UID/GID)がそのまま残るため、別UIDのユーザーで起動するとPermission deniedになることがある
※これはVolume固有の問題というより、Linuxの所有権(数値UID/GID)がそのまま残る、という性質によるものです。

バインドマウントというのを知った

そこで知ったのがバインドマウントでした。

公式:Bind mounts の挙動
https://docs.docker.com/engine/storage/bind-mounts/

Dockerを学び直していると、-vオプションでホストの好きなディレクトリを直接マウントできることを知りました。

docker run -v /host/path:/container/path my-image

これなら、ホスト側のファイルをコンテナから読み書きできる!
しかも、ホスト側で好きなエディタでコードを編集すれば、コンテナ内にもすぐ反映される。

開発中のコードをコンテナで実行したり、ログファイルをホストで確認したりできて、とても便利でした。
「これで解決した!」と思いました。

新たな問題:翌日アクセスできない(権限エラー)

しかし、翌日バインドマウントで同じコンテナを起動してファイルにアクセスしようとすると…

Permission denied

なぜ?昨日は動いていたのに!
ファイルは確かにホスト側にあるし、バインドマウントも正しく設定されているはず…

原因究明:UID/GIDの違い

まず「マウント失敗?」を疑って docker inspectls を見たら、ファイル自体は見えてました。
次に ls -ln を見て「あれ、所有者が“名前”じゃなくて数字…?」となって、
id で自分のUIDと突き合わせたらズレてました。

何が起きていたのか

  1. ホスト側のファイルは私のUID(例:1000)で作成されている
  2. コンテナ内のユーザーは別のUID(例:root=0 や 999など)で実行されている
  3. UIDが異なるため、コンテナからファイルにアクセスできない

Linuxでは、ファイルの所有権はユーザー名ではなくUID/GIDで管理されます。
つまり、たとえ同じ「ユーザー名」でも、UID/GIDが異なれば別のユーザーとして扱われるのです。

ファイル所有者: UID=1010 (testuser1)
    ↓
アクセス者: UID=1000 (appuser)  →  Permission denied
アクセス者: UID=1010 (testuser1) →  アクセス可能

このUIDの不一致が権限エラーの原因です。

Permission deniedの他の要因

権限を緩めるなど、他にも権限エラーの解決方法がありますが今回は割愛します。

解決策:バインドマウント + UID/GID管理

そこで知ったのがバインドマウントUID/GID管理を組み合わせた手法でした。

バインドマウントとVolumeの違い

Dockerには2種類のデータ永続化方法があります:

バインドマウント

Volume

用途に応じた使い分け:

補足:-vと–mountオプションについて

マウント方法を指定するオプションには、-v--mountの2種類があります。

-vオプション(シンプル):

# Volume
docker run -v mydata:/app/data my-image

# バインドマウント
docker run -v /host/path:/app/data my-image

–mountオプション(明示的、Docker公式推奨):

# Volume
docker run --mount type=volume,source=mydata,target=/app/data my-image

# バインドマウント
docker run --mount type=bind,source=/host/path,target=/app/data my-image

--mountの方がタイプが明示的で分かりやすいですが、今回は-vでやります。

docker-composeでの書き方:

services:
  app:
    volumes:
      # ボリュームマウント(名前付き)
      - mydata:/app/data

      # バインドマウント(短縮形)
      - ./host/path:/app/data

      # バインドマウント(長い形式、明示的)
      - type: bind
        source: ./host/path
        target: /app/data

      # ボリュームマウント(長い形式)
      - type: volume
        source: mydata
        target: /app/data

# ボリュームを定義
volumes:
  mydata:

検証(AWS EC2編)

AWS EC2で実際に動作確認してみましょう。

1. EC2インスタンスのセットアップ

# EC2インスタンスにSSH接続後
# 確認
$ whoami
ec2-user

# Dockerのインストール(Amazon Linux 2の場合)
sudo yum update -y
sudo amazon-linux-extras install docker
sudo service docker start
sudo usermod -a -G docker ec2-user

# Docker Composeのインストール
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

# 一度ログアウトして再ログイン(dockerグループ反映のため)
exit

2. サンプルプロジェクトの作成

# 再ログイン後
mkdir ~/bindmount-demo && cd ~/bindmount-demo

# サンプルファイル作成
mkdir src
echo "Hello from host" > src/test.txt

$ ls -ln src/test.txt
-rw-rw-r-- 1 1000 1000 16 Jan  1 07:47 src/test.txt

# 権限を所有者のみに制限(UID不一致の影響を分かりやすくするため)
$ chmod 700 src/test.txt
$ ls -ln src/test.txt
-rwx------ 1 1000 1000 16 Jan  1 07:47 src/test.txt

# 現在のUID/GID確認
echo "Current UID: $(id -u)"
echo "Current GID: $(id -g)"

# Dockerfile作成
cat <<'EOF' > Dockerfile
FROM ubuntu:22.04
RUN groupadd -g 1000 appuser \
 && useradd -u 1000 -g 1000 -m appuser
USER appuser
WORKDIR /app
CMD ["bash"]
EOF

# docker-compose.yml作成
cat <<'EOF' > docker-compose.yml
services:
  app:
    build: .
    volumes:
      - type: bind
        source: ./src
        target: /app/src
EOF

3. バインドマウントの挙動確認

ケース1: UID一致(成功パターン)

# まず、ホスト側のUID/GIDを確認
$ id -u
1000

$ id -g
1000

# ホスト側でファイルの所有者を確認
$ ls -ln src/test.txt
-rwx------ 1 1000 1000 16 Jan  1 07:47 src/test.txt
# ↑ ホスト側: UID=1000, GID=1000

# コンテナをビルド
docker build -t bindmount-demo-app .

# デフォルト設定で起動(UID=1000のappuserで起動)
docker run --rm -it -v ./src:/app/src bindmount-demo-app bash

# コンテナ内で実行
id
uid=1000(appuser) gid=1000(appuser) groups=1000(appuser)

# コンテナ内でファイルの所有者を確認(重要!)
ls -ln /app/src/test.txt
-rwx------ 1 1000 1000 16 Jan  1 07:47 /app/src/test.txt
# ↑ コンテナ内でも: UID=1000, GID=1000 ← ホストと同じ!

cat /app/src/test.txt
Hello from host  ← 読める!

echo "Modified by appuser" >> /app/src/test.txt

exit

# ホスト側で確認
$ cat src/test.txt
Hello from host
Modified by appuser

$ ls -ln src/test.txt
-rwx------ 1 1000 1000 35 Jan  1 08:30 src/test.txt

重要ポイント:

ケース2: UID不一致(エラーパターン)

では、別のユーザー(UID=1010)のファイルにアクセスしてみましょう。

# ホスト側でtestuser1を作成(UID=1010)
$ sudo useradd -u 1010 -m testuser1

# 確認
$ id testuser1
uid=1010(testuser1) gid=1010(testuser1) groups=1010(testuser1)

# testuser1でファイル作成
$ cd ~/bindmount-demo

# srcディレクトリに一時的に書き込み権限を追加 ※検証を簡単にするため一時的に権限を緩めています
$ chmod 777 src/

# testuser1としてファイル作成
$ sudo -u testuser1 sh -c 'echo "Created by testuser1" > src/testuser1-file.txt'

# srcディレクトリの権限を調整(他ユーザーも読み取り・実行可能に)
$ chmod 755 src/

# 作成されたファイルの確認
$ ls -ln src/
total 8
-rwx------ 1 1000 1000 36 Jan  1 11:28 test.txt
-rw-r--r-- 1 1010 1010 21 Jan  1 11:39 testuser1-file.txt

# testuser1-file.txt の詳細確認
$ ls -ln src/testuser1-file.txt
-rw-r--r-- 1 1010 1010 21 Jan  1 11:39 src/testuser1-file.txt
# ↑ UID=1010, GID=1010 (testuser1)

# コンテナを起動(appuser UID=1000)
$ docker run --rm -it -v ./src:/app/src bindmount-demo-app bash

# コンテナ内で確認
appuser@abc123:/app$ id
uid=1000(appuser) gid=1000(appuser) groups=1000(appuser)

# testuser1のファイルの所有者を確認
appuser@abc123:/app$ ls -ln /app/src/testuser1-file.txt
-rw-r--r-- 1 1010 1010 21 Jan  1 11:39 /app/src/testuser1-file.txt
# ↑ コンテナ内でもUID=1010 ← バインドマウントはUID/GIDを変換しない

# 読み取りは可能(パーミッション: rw-r--r--)
appuser@abc123:/app$ cat /app/src/testuser1-file.txt
Created by testuser1

# 書き込みは失敗!(所有者がUID=1010、自分はUID=1000)
appuser@abc123:/app$ echo "Modified by appuser" >> /app/src/testuser1-file.txt
bash: /app/src/testuser1-file.txt: Permission denied

appuser@abc123:/app$ exit
exit

補足:

ケース3: –userオプションで解決

testuser1のファイルにアクセスするには、--user オプションでUID=1010として起動します。

# testuser1と同じUID/GIDで起動(UID:1010, GID:1010)
$ docker run --rm -it -v ./src:/app/src --user 1010:1010 bindmount-demo-app bash
groups: cannot find name for group ID 1010
I have no name!@def456:/app$

I have no name!@def456:/app$ id
uid=1010 gid=1010 groups=1010

I have no name!@def456:/app$ whoami
whoami: cannot find name for user ID 1010

# testuser1のファイルの所有者を確認
I have no name!@def456:/app$ ls -ln /app/src/testuser1-file.txt
-rw-r--r-- 1 1010 1010 21 Jan  1 11:39 /app/src/testuser1-file.txt
# ↑ UID=1010 ← 自分と同じ

# 読み書き可能!
I have no name!@def456:/app$ cat /app/src/testuser1-file.txt
Created by testuser1

I have no name!@def456:/app$ echo "Modified by UID=1010" >> /app/src/testuser1-file.txt

I have no name!@def456:/app$ cat /app/src/testuser1-file.txt
Created by testuser1
Modified by UID=1010

I have no name!@def456:/app$ exit
exit

# ホスト側で確認
$ cat src/testuser1-file.txt
Created by testuser1
Modified by UID=1010

$ ls -ln src/testuser1-file.txt
-rw-r--r-- 1 1010 1010 42 Jan  1 11:47 src/testuser1-file.txt

補足や参考など:

  1. I have no name! について

    • コンテナ内に「UID=1010のユーザーアカウント」は存在しない
    • しかし、UID=1010として動作しているため、ファイルアクセスは可能
    • Linuxカーネルはユーザー名ではなくUID/GIDでファイル権限を判断する
  2. 解決できた理由:

    • --user 1010:1010 でコンテナ内のプロセスをUID=1010で実行
    • testuser1のファイルもUID=1010
    • → UID一致により、アクセス成功

4. バインドマウントの永続性テスト

コンテナを削除して新しいコンテナを起動しても、ホスト側のファイルは永続化されていることを確認します。

重要: --rm オプションをつけると、コンテナ終了時に自動削除されます。しかし、バインドマウントしたホスト側のファイルは削除されず、永続化されます。

# 1回目: コンテナを起動して、ファイルを変更
$ docker run --rm -it -v ./src:/app/src --user 1000:1000 bindmount-demo-app bash

appuser@xyz789:/app$ id
uid=1000 gid=1000 groups=1000

appuser@xyz789:/app$ echo "First session" >> /app/src/test.txt

appuser@xyz789:/app$ cat /app/src/test.txt
Hello from host
Modified by appuser
First session

appuser@xyz789:/app$ exit
exit

# コンテナは終了して自動削除される(--rmオプション)
# しかし、ホスト側のファイルは残っている

# 2回目: 新しいコンテナを起動
$ docker run --rm -it -v ./src:/app/src --user 1000:1000 bindmount-demo-app bash

# 前回のセッションで追加した内容が残っている!
appuser@pqr456:/app$ cat /app/src/test.txt
Hello from host
Modified by appuser
First session
# ↑ 1回目のコンテナで追加した内容が読める

appuser@pqr456:/app$ echo "Second session" >> /app/src/test.txt

appuser@pqr456:/app$ exit
exit

# ホスト側で最終確認
$ cat src/test.txt
Hello from host
Modified by appuser
First session
Second session

確認できたこと:

  1. コンテナは毎回削除されるが、ホスト側のファイルは永続化される
  2. バインドマウントにより、ホストとコンテナでファイルを共有できる
  3. UID/GIDが一致していれば、問題なく読み書き可能
  4. 新しいコンテナを起動しても、以前の変更内容が残っている

5. クリーンアップ

# 検証後のクリーンアップ
cd ~ && rm -rf ~/bindmount-demo

# テストユーザーの削除
sudo userdel -r testuser1

# イメージの削除(オプション)
docker rmi bindmount-demo-app

この手順で、実際にUID/GID問題の発生と解決を体験できます。

まとめ

バインドマウントとUID/GIDの関係を図解すると:

【ホスト側のファイル所有者】  【コンテナ内の実行UID】  【結果】
UID=1000                      UID=1000             ✓ アクセス可能
UID=1010 (testuser1)         UID=1000 (appuser)   ✗ Permission denied

重要ポイント:

複数ユーザーでの管理方法については、次回の記事で紹介予定です。

以前は「なぜ権限エラーが…?」と悩みましたが、バインドマウントはUID/GIDを変換しないという仕組みを理解することで解決できました。

同じような問題に遭遇している方の参考になれば幸いです!

参考 UID/GIDを確認する方法

# ホスト側で自分のUID/GIDを確認
id
# uid=1000(username) gid=1000(username) groups=...

# または
id -u  # UID のみ表示: 1000
id -g  # GID のみ表示: 1000

# ホスト側のファイル所有者を確認(数値で表示)
ls -ln /host/path
# -rw-r--r-- 1 1000 1000 ... ← UID=1000, GID=1000

# コンテナ内で実行ユーザーのUID/GIDを確認
docker run ubuntu:22.04 id
# uid=0(root) gid=0(root) groups=0(root)

# または、コンテナ内に入って確認
docker run -it ubuntu:22.04 bash
# id
# uid=0(root) gid=0(root) ...

例えば、ホスト側がuid=1010(testuser1)、コンテナ内がuid=1000(appuser)の場合:

# ホスト側(testuser1のファイル)
ls -ln src/testuser1-file.txt
# -rw-r--r-- 1 1010 1010 21 Jan  1 11:39 src/testuser1-file.txt
# ↑ UID=1010

# コンテナ内(appuserで実行)
docker run --rm -it -v ./src:/app/src bindmount-demo-app bash
# id
# uid=1000(appuser) gid=1000(appuser) ...
# ↑ UID=1000 ← 不一致!Permission deniedになる

参考リンク

モバイルバージョンを終了