Docker Composeのvolumesを使ってもっと効率的に

[Docker Composeを使った初期装備なRails6/PostgreSQL環境の作り方 \- Qiita](https://qiita.com/tanaken0515/items/0e207160c25a6b815561) の続編です。

前回の記事では、[Quickstart: Compose and Rails \| Docker Documentation](https://docs.docker.com/compose/rails/)に沿ってDocker Composeを使ったRuby2.7,Rails6,PostgreSQLでの開発環境を作る方法について紹介しました。

この記事では、前回の記事で構築した開発環境における問題点（不便な点）について述べたあと、Docker Composeのvolumesを使ってその問題を解決する方法について記載します。

想定する読者は前回と同じです。

> - Rails環境構築でいつも躓くの方（丁寧めに説明します）
> - 未来の自分（思い出せるように書きます）

## 問題点

結論から言うと、前回作った開発環境には**Gemを追加する度にイメージをbuildしないといけない**という問題点があります。

具体的に[pry](https://github.com/pry/pry/)（IRBの代替となるGemです）を追加してみて、その流れの中で詳しく説明します。


### Gemをインストールしてみる
まずGemfileにpryを追記してください。

```diff:Gemfile
 gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
+gem 'pry'
```

次にwebのコンテナに入ってgemをインストールしましょう。

```sh
$ docker-compose run web bash
Starting docker-compose-rails-sample_db_1 ... done
root@4c3d93084e7c:/myapp# bundle install
.......
```

インストールが完了するとGemfile.lockがこんな感じで書き変わると思います。

```diff:Gemfile.lock
@@ -72,6 +72,7 @@ GEM
       regexp_parser (~> 1.5)
       xpath (~> 3.2)
     childprocess (3.0.0)
+    coderay (1.1.3)
     concurrent-ruby (1.1.6)
     crass (1.0.6)
     erubi (1.9.0)
@@ -102,6 +103,9 @@ GEM
     nokogiri (1.10.9)
       mini_portile2 (~> 2.4.0)
     pg (1.2.3)
+    pry (0.13.1)
+      coderay (~> 1.1)
+      method_source (~> 1.0)
     public_suffix (4.0.5)
     puma (4.3.5)
       nio4r (~> 2.0)
@@ -204,6 +208,7 @@ DEPENDENCIES
   jbuilder (~> 2.7)
   listen (~> 3.2)
   pg (>= 0.18, < 2.0)
+  pry
   puma (~> 4.1)
   rails (~> 6.0.3, >= 6.0.3.1)
   sass-rails (>= 6)
```

pryがインストールされていることを確認しましょう。先ほどのコンテナの中でpryを実行してください。

```sh
root@4c3d93084e7c:/myapp# pry
[1] pry(main)> 
```

ちゃんとインストールされていますね！ではコンテナから出ましょう。

```sh
root@4c3d93084e7c:/myapp# exit
```

ここまでは特に問題なさそうにみえますね。

### 問題を確認する

改めてwebのコンテナに入り直してみましょう。

```sh
$ docker-compose run web bash
Starting docker-compose-rails-sample_db_1 ... done
root@c91cec1a5be0:/myapp#
```

さっきpryをインストールしたので、pryを起動して確認してみましょう。

```sh
root@c91cec1a5be0:/myapp# pry
bash: pry: command not found
```

**！！！pryが入っていない！！！**

### なぜこの問題が起こるのか

端的にいえば「**pryをインストールしたコンテナとは別のコンテナの中にいるから**」です。

pryをインストールした時は`root@4c3d93084e7c:/myapp`で、先ほど確認した時は`root@c91cec1a5be0:/myapp`ですね。
`4c3d93084e7c`や`c91cec1a5be0`はコンテナのIDで、`docker ps -a`で確認することができます。

```sh
$ docker ps -a
CONTAINER ID        IMAGE                             COMMAND                  CREATED             STATUS                           PORTS                                            NAMES
c91cec1a5be0        docker-compose-rails-sample_web   "entrypoint.sh bash"     20 minutes ago      Up 20 minutes                    3000/tcp                                         docker-compose-rails-sample_web_run_f7eaa6128b30
4c3d93084e7c        docker-compose-rails-sample_web   "entrypoint.sh bash"     About an hour ago   Exited (0) 22 minutes ago                                                         docker-compose-rails-sample_web_run_8db7cf6c1c98
```

この問題、dockerについて理解していれば「そりゃそうだよね〜」という感じですね。

一応ざっくり説明をしておくと、Dockerfileに基づいてイメージ(IMAGE)が作られ、イメージに基づいてコンテナ(CONTAINER)が作られます。（詳しくは[Docker overview \| Docker Documentation](https://docs.docker.com/get-started/overview/)）

今回の例だと、pryがインストールされていない状態のイメージがあり、それに基づいてコンテナを作っているため、それぞれのコンテナはpryがインストールされていない状態で作られます。

それぞれのコンテナは独立しているので、コンテナ`4c3d93084e7c`でpryをインストールしても、コンテナ`c91cec1a5be0`には関係なく、pryがインストールされていないままの状態になっています。

これを解決するには"pryがインストールされていない状態のイメージ"を"pryがインストールされている状態のイメージ"に更新する必要があります。以下のコマンドを実行するとイメージを作りなおしてくれます。（次節の説明の都合上、**実行しないで**ください）

```sh
$ docker-compose build
```

作りなおしが完了したあとにコンテナに入ると、pryがインストールされた状態になります。

これで解決！、としてもいいのですが、これだとGemを追加する度に毎回buildする必要があって効率的じゃありません。
今回の場合だと（僕の環境では）イメージのサイズが1.35GBあって、これを毎回作り直すのはPCにも優しくなさそうです。

```sh
$ docker images
REPOSITORY                        TAG                        IMAGE ID            CREATED             SIZE
docker-compose-rails-sample_web   latest                     a889fd5e81fa        2 hours ago         1.35GB
```

**イメージをbuildすることなくGemの追加を反映したい！**という気持ちになります。
コンテナ間でGemのインストール内容を共有できる仕組みがあれば実現できそうです。

## 解決方法
Docker Composeのvolumesを使うと解決することができます。

[Overview of Docker Compose \| Docker Documentation](https://docs.docker.com/compose/#preserve-volume-data-when-containers-are-created)の "Preserve volume data when containers are created" より、
> When docker-compose up runs, if it finds any containers from previous runs, it copies the volumes from the old container to the new container. This process ensures that any data you’ve created in volumes isn’t lost.  

とのこと。「docker-composeを実行するとき、**過去に実行したコンテナがあればそのコンテナからvolumesを新しいコンテナにコピー**するよ。このおかげで**volumesに作成したデータは失われない**ことが保証されるよ。」という感じですね。

### 書き換えてみる

ではさっそく、[Compose file version 3 reference \| Docker Documentation](https://docs.docker.com/compose/compose-file/#volume-configuration-reference)の "Volume configuration reference" を読みながら、`docker-compose.yml`をこんな感じで書き換えてみることにします。

```diff:docker-compose.yml
 version: '3'
 services:
   db:
     image: postgres:12.3
     volumes:
       - ./tmp/db:/var/lib/postgresql/data
     environment:
       POSTGRES_HOST_AUTH_METHOD: trust
   web:
     build: .
     command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
     volumes:
       - .:/myapp
+      - bundle:/usr/local/bundle
     ports:
       - "3000:3000"
     depends_on:
       - db
+volumes:
+  bundle:
```

一番下の`volumes`では、`bundle`という名前のvolumeを定義しています。
（`bundle: `となっていて「書き忘れ？」感がありますが、これは「本当はドライバを指定できるのだけど指定しなくても良いよ。未指定の場合はdockerエンジンのデフォルトのドライバ（大抵は`local`ドライバ）が使われるよ」という仕様に基づいています。）

そして`services > web > volumes`では、「`bundle`という名前のvolumeをコンテナ内の`/usr/local/bundle`にマウントするよ」と定義しています。

この設定によって、コンテナ内で`/usr/local/bundle`にインストールされたRubyGemsは、`bundle`という名前のvolumeに記録され、このvolumeは新しいコンテナを作成したときにコピーされて使いまわされるようになります。


### 動作を確認する

では実際の動きを確認してみましょう。まずはwebコンテナでbashを起動します。

```sh
$ docker-compose run web bash
Creating volume "docker-compose-rails-sample_bundle" with default driver
Starting docker-compose-rails-sample_db_1 ... done
root@2e32d1dbc2c4:/myapp# 
```

> Creating volume "docker-compose-rails-sample_bundle" with default driver

というログが出てますね。まさにvolumeを作ったぜ、ということです。

「問題点」の節でも確認しましたが、この状態ではまだpryが入っていないはずです。

```sh
root@2e32d1dbc2c4:/myapp# pry
bash: pry: command not found
```

さて、Gemをインストールし、pryが入ったことを確認しましょう。

```sh
root@2e32d1dbc2c4:/myapp# bundle install
root@2e32d1dbc2c4:/myapp# pry
[1] pry(main)>
```

ちゃんと入りましたね。そしたらこのコンテナから出て、

```sh
root@2e32d1dbc2c4:/myapp# exit
```

新たにコンテナを起動しましょう。そしてpryを打ってみると、、

```sh
$ docker-compose run web bash
Starting docker-compose-rails-sample_db_1 ... done
root@f834744056f7:/myapp# pry
[1] pry(main)> 
```

おぉ〜、入ってる。Gemがインストールされた状態を維持できています。素晴らしいですね〜:tada::tada::tada:

### ついでにnode_modulesも

RubyGemsの話だけでいうとここまでで良いのですが、今回作っているRails6の環境ではyarnでインストールするライブラリ群についても同様の問題があります。
ついでなのでそれらについても対応してしまいましょう。

```diff:docker-compose.yml
     volumes:
       - .:/myapp
       - bundle:/usr/local/bundle
+      - node_modules:/myapp/node_modules
     ports:
       - "3000:3000"
     depends_on:
       - db
 volumes:
   bundle:
+  node_modules:
```

`node_modules`というvolumeを定義して`/myapp/node_modules`にマウントしているだけです。簡単ですね。

なお、今回の記事での変更点はこのプルリクエストに記録しました。
https://github.com/tanaken0515/docker-compose-rails-sample/pull/4

## まとめ

今回の記事では、前回の記事で構築した開発環境におけるについて、「Gemを追加するときに毎回イメージをbuildする必要がある」という問題点を解説した上で、Docker Composeのvolumesを使ってその問題を解決する方法を記載しました。参考になれば幸いです。

個人的にもリファレンスをちゃんと読みながら作業したので勉強になりました、よかったよかった。

なお、この記事の続編として以下のような記事を書いていこうかなと思っています。

- [Docker Composeで作った初期装備なRails6にVue\.jsを導入する \- Qiita](https://qiita.com/tanaken0515/items/a6e9511fc6d8c0065dc1) <- 書きました(2020-06-18)
- Docker Composeでwebpack-dev-serverを動かしてもっと快適に

（予定は未定です）

ではまた〜