Docker Compose + Railsでイメージ内でbundle installしているはずなのにgemが無いとエラーがでる。

# 背景
docker-composeでrails環境を構築したが起動せず。
gemが無いと言われているので`docker-compose run 'コンテナ' bundle install`の実行で解決。
けどなんでDockerfile内で`bundle install`しているのに再度`bundle install`しないといけないんだろうと疑問に思ったので調査してみました。

# 先に結論
volumeに古い情報が保存されており、最新の情報がvolumeに上書きされてしまう。

# エラー再現例
## 構築時のファイル例
```Dockerfile:Dockerfile
FROM ruby:2.6.3

RUN apt-get update -qq && \
    apt-get install -y build-essential \
                       libpq-dev \
                       postgresql-client &&\
    rm -rf /var/lib/apt/lists/*

# install nodejs
RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - \
&& apt-get install -y nodejs

# install yarn
RUN apt-get update && apt-get install -y curl apt-transport-https wget && \
    curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
    echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
    apt-get update && apt-get install -y yarn

ENV APP_HOME /my_app
RUN mkdir -p $APP_HOME
WORKDIR $APP_HOME
ADD Gemfile Gemfile
ADD Gemfile.lock Gemfile.lock

RUN bundle install

ADD . $APP_HOME
```

```yaml:docker-compose.yml
version: '3'

services:
  web:
    build: .
    ports:
      - "3000:3000"
    command:
      [ "bash", "-c", "rm -f tmp/pids/server.pid; RAILS_ENV=development bundle exec rails s -b 0.0.0.0" ]
    volumes:
      - .:/my_app
      # volumeを使用してbundle installしてきたものを永続化
      - bundle:/usr/local/bundle 
volumes:
  bundle:
```


```:Gemfile
source 'https://rubygems.org'
gem 'rails', '6.0.0'
```

空のGemfile.lock

```
$ touch Gemfile.lock
```

## よくある構築コマンド例
```shell
# railsプロジェクトを作成
$ docker-compose run web bundle exec rails new . --force --skip-bundle

# Dockerfileからイメージを作成
$ docker-compose build

# コンテナ（rails）の起動
$ docker-compose up
```
を実行すると

```
web_1  | bundler: failed to load command: rails (/usr/local/bundle/bin/rails)
web_1  | Bundler::GemNotFound: Could not find gem 'sqlite3 (~> 1.4)' in any of the gem sources listed in your Gemfile.
...
```
と出て起動しない。

コンテナに入ってgemを確認してみると

```
$ docker-compose run web ls /usr/local/bundle/gems
actioncable-6.0.0      erubi-1.8.0          rack-test-1.1.0
actionmailbox-6.0.0    globalid-0.4.2       rails-6.0.0
actionmailer-6.0.0     i18n-1.6.0           rails-dom-testing-2.0.3
actionpack-6.0.0       loofah-2.2.3         rails-html-sanitizer-1.2.0
actiontext-6.0.0       mail-2.7.1           railties-6.0.0
actionview-6.0.0       marcel-0.3.3         rake-12.3.3
activejob-6.0.0        method_source-0.9.2  sprockets-3.7.2
activemodel-6.0.0      mimemagic-0.3.3      sprockets-rails-3.2.1
activerecord-6.0.0     mini_mime-1.0.2      thor-0.20.3
activestorage-6.0.0    mini_portile2-2.4.0  thread_safe-0.3.6
activesupport-6.0.0    minitest-5.11.3      tzinfo-1.2.5
builder-3.2.3          nio4r-2.5.0          websocket-driver-0.7.1
concurrent-ruby-1.1.5  nokogiri-1.10.4      websocket-extensions-0.1.4
crass-1.0.4            rack-2.0.7           zeitwerk-2.1.9
```
`docker-compose build`のときに`bundle install`したはずなのに`gem 'rails'`でインストールしたgemしか入っていない。（`rails new`で更新されたGemfileのgemがインストールされていない）

# なぜ起きるのか
下記条件で発生します。

- `bundle install`してきた内容をvolumeを使用してデータを永続化をしている。
- rails newで--skip-bundleしている。

## ポイント
- `docker-compose build`ではvolumeと紐付かない
- `docker-compose run`はvolumeと紐づく。


## 原理

```shell
# 初回のbuildが走る。bundle installで ”railsのみ” がコンテナ内の/usr/local/bundleにインストールされる。
# volumeと紐づくので ”railsのみ” がvolumeに保存される。
$ docker-compose run web bundle exec rails new . --force --skip-bundle

# 新しいGemfileのgemがコンテナ内の/usr/local/bundleにインストールされる。
# volumeと紐づかないのでvolumeは更新されない。
$ docker-compose build

# volumeと紐付いたタイミングで/usr/local/bundleがvolumeに上書かれてしまう。
$ docker-compose up
```


# 対応
- `docker-compose run web bundle install`でvolumeを更新する。
- volumeを一回削除し再度作成することによって、volumeの中身を/usr/local/bundleと同じにする。
- `rails new`する時に`--skip-bundle`を使用しない。

# 疑問
> docker-compose run 'コンテナ' bundle installをどうせ走らせるのならDockerfileからbundle installを削除していいのでは。

イメージとdocker-composeを1セットと考えるとそう思いますが、同じイメージを使ってvolumeを使わないパターンもあり得るので`bundle install`をDockerfileから削除するのが必ずしも正しいわけではないのかなと感じました。
開発する上で下記を意識すれば問題ないかと思います。

- gemをvolumeで管理しない：`docker-compose build`でgemを管理する。
- gemをvolumeで管理する：`docker-compose run 'コンテナ' bundle install`でgemを管理する。（`docker-compose build`で`bundle install`が走るのは我慢する？）


# さいごに
Qiitaとかにあるチュートリアルをそのまま実行して起動できなくてハマった方もいると思うので、少しでも参考になればと思います。
間違えや不足などあればご指摘頂ければ嬉しいです。