docker

dockerでvolumeをマウントしたときのファイルのowner問題

dockerでvolumeをマウントするときの問題点

docker runするときに-vオプションをつけることによってホストのディレクトリをコンテナ内にマウントすることができる。
ホスト側のファイルをコンテナ内で使いたい場合や、逆にコンテナで作ったファイルにホストからアクセスしたい場合に有用なのだが、ファイルのアクセス権限についてちゃんと考えておかないと問題が起きることがある。

例えば、ホスト内でのユーザーのuidが500だったとしよう。

$ id
uid=500(ec2-user) gid=500(ec2-user) groups=500(ec2-user),10(wheel),497(docker)

そこで、volumeをマウントしたコンテナを作ってみる。

$ mkdir -p temp && touch temp/foo                       # 実験用に適当なディレクトリを作ってみる
$ docker run -it -v $(pwd)/temp:/temp busybox    # 先ほど作ったディレクトリを /temp にマウントする
(コンテナ内にて)
# ls -la
total 48
drwxr-xr-x    1 root     root          4096 Mar  6 23:58 .
drwxr-xr-x    1 root     root          4096 Mar  6 23:58 ..
-rwxr-xr-x    1 root     root             0 Mar  6 23:58 .dockerenv
drwxr-xr-x    2 root     root         12288 Feb 20 17:57 bin
(省略)
drwxrwxr-x    2 500      500           4096 Mar  6 23:56 temp
(省略)

このように、コンテナ内からはマウントしたファイルのownerがuid=500,gid=500になっている。ここでファイルを作成してみる。

# touch temp/bar
# ls -l temp
total 0
-rw-r--r--    1 root     root             0 Mar  7 00:02 bar
-rw-rw-r--    1 500      500              0 Mar  6 23:56 foo

となり、rootがownerのファイルとして作成される。
ホストOSから同じファイルを見て見ると

$ ls -l temp
total 0
-rw-r--r-- 1 root     root     0 Mar  7 00:02 bar
-rw-rw-r-- 1 ec2-user ec2-user 0 Mar  6 23:56 foo

となり、rootの持ち物として作成される。当然このファイルは一般ユーザーからは編集不可になってしまう。

コンテナとホストで相互にファイルのやりとりをしたいときにこの挙動は困ることが多い。

ちなみにdocker for macで試したところ、上記の問題は起きなかった。
コンテナ内からはownerがrootとして表示されるが、mac上からは自ユーザーがownerとして表示されている。docker for macの中でうまく解決してくれているようだ。
以下はlinuxの場合の対処法。

解決策

この問題の解決策として以下のページで示されていた方法を紹介する。
参考 : https://denibertovic.com/posts/handling-permissions-with-docker-volumes/

まずはうまくいかない方法から紹介する。

うまくいかない方法1 : Dockerfile内でuseraddする

例えば

RUN  useradd --shell /bin/bash -u 1024 -o -c "" -m myuser
RUN mkdir -p /shared/tmp && chown user. /shared/ -R
USER myuser
CMD /usr/local/bin/myprocess

この方法はイメージをビルドしたマシンと実行するマシンが同じならば問題がない。
しかし、イメージをビルドする段階でuidを決定しなければならないので、別のマシンでビルドしたイメージは使えず実用的ではない。

うまくいかない方法2 : docker runに-uオプションをつける

docker runコマンドには実行するユーザーを指定する-uオプションがある。この-uでホストOS上のUIDを指定すればよいような気がする。

$ docker run -it -u `id -u $USER` debian:jessie /bin/bash
I have no name!@dcb415bad433:/$ id
uid=500 gid=0(root) groups=0(root)

こうするとコンテナ内のUIDがホストOSと同じUIDになる。しかし

  • gid (group id)が変わっていない
  • /etc/passwd の情報とuidが一貫していない

という問題がある。2つ目の問題はファイルを使っているだけなら問題ないが、いくつかのアプリで/etc/passwdを参照することがあり問題が起きることもある。
つまり、-uのようなオプションでuidを指定したいが、実際にuseraddを使ってユーザーを作ることが必要となる。

うまくいく方法1 : ENTRYPOINTでuseraddでユーザーを作る

基本的な方針は

  • ホストOSでのユーザーのUIDを環境変数で渡す(必要であればGIDも)
  • コンテナ内でuseraddでuidを指定して一般ユーザーを作る
  • その一般ユーザーでコマンドを実行する

となる。

Dockerfile
FROM ubuntu:latest

RUN apt-get update && apt-get -y install gosu
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
entrypoint.sh
#!/bin/bash

USER_ID=${LOCAL_UID:-9001}
GROUP_ID=${LOCAL_GID:-9001}

echo "Starting with UID : $USER_ID, GID: $GROUP_ID"
useradd -u $USER_ID -o -m user
groupmod -g $GROUP_ID user
export HOME=/home/user

exec /usr/sbin/gosu user "$@"

これらのファイルを準備してdocker build . -t mybase コマンドでmybaseイメージを作る。

entrypoint.shの中では、環境変数LOCAL_UIDで指定されたuidでユーザーを作っている。
gosuというツールはgoで書かれたsuコマンドのようなもの。普通のsuコマンドはTTYの問題とか色々と奇妙なことがおきるらしいので、それを回避するためのもの。やっていることはsuと同じだと思えばよい。
(参照元の記事ではgosuをインストールする手順がもっと複雑だが、最新のubuntuイメージの場合にはapt-get install -y gosuするだけでOK)

また参照元記事ではGIDは特に設定していないが、今回はentrypoint.shでGIDも参照するようにした。

試しにこのイメージを実行して見る。

$ docker run -it mybase bash
Starting with UID : 9001
user@d45b2d3198c9:/$ id
uid=9001(user) gid=9001(user) groups=9001(user)

今は環境変数を何もしていないので、デフォルトの9001番でユーザーが作られている。

では環境変数を設定して、ボリュームをマウントして見る。

$ docker run -it -v $(pwd)/temp:/temp -e LOCAL_UID=$(id -u $USER) -e LOCAL_GID=$(id -g $USER) mybase bash
# コンテナにて
$ id
uid=500(user) gid=500(user) groups=500(user)
$ ls -l /temp
total 0
-rw-rw-r-- 1 user user 0 Mar  7 01:49 foo

となり、ホストOS上でのUID,GIDが適切に設定されるようになったことがわかる。
コンテナ内でマウントされたディレクトリにファイルを作成しても、ホストOSからは自ユーザーがownerのファイルのように見える。
コンテナ内とホストOSではユーザー名は異なるが、ファイルシステムはUIDを使ってファイルを管理しているのでこれでうまくいく。

うまくいく方法2 : /etc/passwdと/etc/groupをコンテナにマウントする

別の方法。docker run-uをつけてもうまくいかなかったのは、/etc/passwdとの不整合が起きるためであった。
これがうまくいくようにするには、ホストOSの/etc/passwdをマウントするという方法がある。

docker run -it -v /etc/group:/etc/group:ro -v /etc/passwd:/etc/passwd:ro -u $(id -u $USER):$(id -g $USER) ubuntu bash

ここではコンテナが勝手に/etc/passwdや/etc/groupを書き換えないようにread onlyでマウントしている。
これでほとんどの場合うまくいく。(ただし、上記のコマンドはDocker for macでは失敗する。/etcがマウントできないため)

しかし、コンテナのイメージを作成する際にすでに一般ユーザーを作って作業を行っている場合、その一般ユーザーで作ったファイルにアクセスできなくなるため問題が起きる。

うまくいく方法3: Dockerfile内ですでに一般ユーザーが作られている場合

イメージを作る段階ですでに一般ユーザーを作って、そのホーム以下でいろいろと設定しているケースを考える。
その場合、すでにコンテナ内に存在する一般ユーザーのUIDをusermodコマンドで変更し、ホストOSのUIDと合わせるとよい。

方法1とほぼ同様に、スクリプトを以下のように準備する。usermodコマンドで既存ユーザーのUIDを変更している。
このとき、usermodコマンドはHOME以下のファイルのownerも自動的に切り替えてくれるのでファイルのオーナーを書き換えたりする必要はない。

entrypoint.sh
usermod -u $USER_ID -o -m user
groupmod -g $GROUP_ID user

まとめ

dockerでvolumeをマウントするとファイルのUIDを適切に設定しなくてはいけない。
ほとんどの場合は方法2が最も簡単だが、コンテナのイメージにすでに一般ユーザーが作られている場合は方法3を使うのが良さそう。