rails(Docker)+nginxでPermission denied @ rb_io_reopen - /myapp/log/puma.stdout.log (Errno::EACCES)が発生する


# はじめに

Rails環境をDockerを利用して環境構築していたのですが、Nginxをつないだところエラーが発生してほかの記事があまり参考にならなかったため自分なりにまとめていきます。

主にWSL2でDockerを使っている方がこのエラーの対象になるかと思います。

# 問題

DockerでRailsとNginxの環境を用意して、Railsコンテナのユーザーを`app`(rootではない)にdocker-compose.ymlで変更した状態でコンテナを起動すると以下のエラーが発生します。

```
rails        | * Listening on http://0.0.0.0:3000
rails        | * Listening on unix:///myapp/tmp/sockets/puma.sock
rails        | Use Ctrl-C to stop
rails        | bundler: failed to load command: puma (/usr/local/bundle/bin/puma)
rails        | /usr/local/bundle/gems/puma-5.5.2/lib/puma/runner.rb:123:in `reopen': Permission denied @ rb_io_reopen - /myapp/log/puma.stdout.log (Errno::EACCES)
rails        |  from /usr/local/bundle/gems/puma-5.5.2/lib/puma/runner.rb:123:in `redirect_io'
rails        |  from /usr/local/bundle/gems/puma-5.5.2/lib/puma/single.rb:56:in `run'
rails        |  from /usr/local/bundle/gems/puma-5.5.2/lib/puma/launcher.rb:181:in `run'
rails        |  from /usr/local/bundle/gems/puma-5.5.2/lib/puma/cli.rb:80:in `run'
rails        |  from /usr/local/bundle/gems/puma-5.5.2/bin/puma:10:in `<top (required)>'
rails        |  from /usr/local/bundle/bin/puma:25:in `load'
rails        |  from /usr/local/bundle/bin/puma:25:in `<top (required)>'
rails        |  from /usr/local/lib/ruby/3.0.0/bundler/cli/exec.rb:58:in `load'
rails        |  from /usr/local/lib/ruby/3.0.0/bundler/cli/exec.rb:58:in `kernel_load'
rails        |  from /usr/local/lib/ruby/3.0.0/bundler/cli/exec.rb:23:in `run'
rails        |  from /usr/local/lib/ruby/3.0.0/bundler/cli.rb:478:in `exec'
rails        |  from /usr/local/lib/ruby/3.0.0/bundler/vendor/thor/lib/thor/command.rb:27:in `run'
rails        |  from /usr/local/lib/ruby/3.0.0/bundler/vendor/thor/lib/thor/invocation.rb:127:in `invoke_command'
rails        |  from /usr/local/lib/ruby/3.0.0/bundler/vendor/thor/lib/thor.rb:392:in `dispatch'
rails        |  from /usr/local/lib/ruby/3.0.0/bundler/cli.rb:31:in `dispatch'
rails        |  from /usr/local/lib/ruby/3.0.0/bundler/vendor/thor/lib/thor/base.rb:485:in `start'
rails        |  from /usr/local/lib/ruby/3.0.0/bundler/cli.rb:25:in `start'
rails        |  from /usr/local/lib/ruby/gems/3.0.0/gems/bundler-2.2.32/libexec/bundle:49:in `block in <top (required)>'
rails        |  from /usr/local/lib/ruby/3.0.0/bundler/friendly_errors.rb:103:in `with_friendly_errors'
rails        |  from /usr/local/lib/ruby/gems/3.0.0/gems/bundler-2.2.32/libexec/bundle:37:in `<top (required)>'
rails        |  from /usr/local/bin/bundle:23:in `load'
rails        |  from /usr/local/bin/bundle:23:in `<main>'
rails exited with code 1
```

`Permission denied @ rb_io_reopen - /myapp/log/puma.stdout.log (Errno::EACCES)`ということでpuma.stdout.logが権限的にみれないよとエラーを出しています。

# 調査

これは`puma`のログがコンテナ内でroot権限で作成されることが原因です。

私の作成した`docker-compose.yml`は以下のようになっています。

```docker-compose.yml
version: "3.9"
services:
  rails:
    build: .
    container_name: rails
    command: bundle exec puma -C config/puma.rb
    volumes:
      - .:/myapp
      - public-data:/myapp/public
      - tmp-data:/myapp/tmp
      - log-data:/myapp/log
    env_file:
      - .env
    depends_on:
      - db
    environment:
      WEBPACKER_DEV_SERVER_HOST: webpacker
    user: app

  webpacker:
    build: .
    volumes:
      - .:/myapp
    command: ./bin/webpack-dev-server
    environment:
      WEBPACKER_DEV_SERVER_HOST: 0.0.0.0
    ports:
      - "3035:3035"

  db:
    image: mysql:8.0.27
    container_name: db
    environment:
      TZ: Asia/Tokyo
      MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
    ports:
      - "3306:3306"
    volumes:
      - db:/var/lib/mysql
  
  web:
    build:
      context: containers/nginx
    volumes:
      - public-data:/myapp/public
      - tmp-data:/myapp/tmp
    ports:
      - 80:80
    depends_on:
      - rails

volumes:
  db:
    driver: local
  bundle:
    driver: local
  public-data:
  tmp-data:
  log-data:
```

ここで注目してほしいのは`rails`コンテナの`user: app`という箇所でユーザーを指定しているところです。これでコンテナ内でrails gなどでファイル生成しても`appユーザー`(uid=1000)で作成されるため、WSL2のユーザーと権限が同じなので編集ができるようになります。この設定をしないとrootユーザーでファイルが作成されるためWSL2ユーザーでは編集ができません。

しかし、`app`を指定したことで`puma`ログが`root`権限で作成されているため編集できずエラーとなっていました。

ここで考えられるのは以下の方法です。

- WSL2をrootでログインするように設定する
- コンテナ内の参照するフォルダ(と中身のファイル)の権限をappユーザーが開けるように変更する

今回はコンテナ内の`/{workdir}/log`(ここではmyapp/log)の権限を変更して対応していきます。

まずは実際に権限がrootになっているのか確認します。
いまのままではコンテナが落ちてしまうので`docker-compose.yml`のrailsコンテナのコマンドを以下に変更します。

```docker-compose.yml
command: sh -c "while :; do sleep 10; done" && bundle exec puma -C config/puma.rb
```
コンテナの中に入って以下のコマンドを打ちます。

```
# コンテナの起動
# $ docker-compose up
# コンテナに入る
# $ docker exec -it rails sh

# 権限の確認
$ ls -la
```

```
# rootになっている
drwxr-xrwx    2 root     root          4096 Jan 16 08:49 log
```

この権限を`app`に変更できれば良さそうです。

https://qiita.com/yohm/items/047b2e68d008ebb0f001

こちらの記事によると、Volumesで設定すると`root`になってしまうようです。
ここで2つの回避策を考えましたので紹介します。

# 回避策1 コンテナの中で権限を変更する

コンテナの中で直接権限を変更します。
注意としては`root`でコンテナに入る必要があるので、docker-compose.ymlの`user`をコメントにします。

rootであれば権限問題が起きないのでコンテナは立ち上がるかと思います。

```
# 別ターミナルを開く
# コンテナに入る
$ docker exec -it rails sh
# 権限を変更する
$ chown app:app -R /myapp/log
```

chownでエラーが出る場合は`root`ユーザーになっていないのが原因かと思いますので`id -u -n`コマンドで確認してください。

一度変更すれば以降は変更された状態になります。
しかし、ボリュームを削除したり、gitからダウンロードした場合にrootでアクセスしてdocker-compose.ymlを編集してuser:appに変更するという手間も発生するので微妙かなとは思いました。

# 回避策2 Nginxで権限変更のシェルを実行する

1の欠点を補うためにNginxコンテナを起動するときにシェルを走らせることで回避することにしました。
まず準備として`docker-compose.yml`のNginxのボリュームを変更します。

```docker-compose.yml
  web:
    build:
      context: containers/nginx
    volumes:
      - public-data:/myapp/public
      - tmp-data:/myapp/tmp
      - log-data:/myapp/log
    ports:
      - 80:80
    depends_on:
      - rails

```

`myapp/log`をvolumesに設定しました。
そして、`Dockerfile`で`entrypoint.sh`を設定できるようにします。

```Dockerfile
FROM nginx:alpine

# インクルード用のディレクトリ内を削除
RUN rm -f /etc/nginx/conf.d/*
RUN adduser -D app -u ${UID:-1000}

COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]

# Nginxの設定ファイルをコンテナにコピー
ADD nginx.conf /etc/nginx/conf.d/myapp.conf

# ビルド完了後にNginxを起動
CMD /usr/sbin/nginx -g 'daemon off;' -c /etc/nginx/nginx.conf
```

次にnginxのDockerfileと同じディレクトリに`entrypoint.sh`を作成します。

```entrypoint.sh
#!/bin/sh
set -e
chown app:app -R /myapp/log
exec "$@"
```

ここまでの設定を行うことで、Nginxコンテナが立ち上がったときに`entrypoint.sh`が実行され、volumesで設定されているmyapp/logの権限をappに変更します。また同じユーザーをDockerfileで作成することも忘れないようにしてください。

nginx自体はrootユーザーで起動するため権限変更`chown`が利用でき変更が可能です。
railsコンテナで同じように権限を変更しようとなると`root`でアクセスする必要があるため今回はnginxで権限を変更するように設定しました。

# 回避策3 マウントに権限を付与する

マウントで権限を与える方法が分かりました。
`tmpfs`マウントを使うと権限を与えることが可能になります。
今回はコンテナ間で共有しないのでこれで良さそうです。

```docker-compose.yml
tmpfs:
  - /myapp/log:exec,mode=777,uid=1000,gid=1000
```

# 解決策

解決策はいたってシンプルでDockerfileでユーザーを追加して、ディレクトリコピーの時に`chown`すればOKでした。

```Dockerfile
FROM ruby:alpine3.13

ARG UID

RUN adduser -D app -u ${UID:-1000} && \ # user追加
      apk update \
      && apk add --no-cache gcc make libc-dev g++ mariadb-dev tzdata nodejs~=14 yarn

WORKDIR /myapp
COPY Gemfile .
COPY Gemfile.lock .
RUN bundle install
COPY --chown=app:app . /myapp # 権限を付与してコピー
RUN chown app:app -R /myapp/node_modules/.bin
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
# EXPOSE 3000
# CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"]

USER app # ユーザー変更
RUN mkdir -p tmp/sockets # フォルダ作成はユーザー設定してから
RUN mkdir -p tmp/pids
```

# おわりに

権限問題が複雑に絡まっており解決するまで時間がかかりました。
調べてもよくわからず自分なりに工夫をして解決することができました。

# 参考
- [dockerでvolumeをマウントしたときのファイルのowner問題](https://qiita.com/yohm/items/047b2e68d008ebb0f001)