Railsチュートリアルの開発環境を Docker でもっと便利にしなイカ！？

# 【概要】
本記事では、Gem の永続化、データベース及びテスト用メールサーバをコンテナ化していきます。
[Railsチュートリアルの開発環境を Docker にしてみなイカ？](https://qiita.com/Moo_Moo_Farm/items/0d08da27371272ed1390)の続きです。

さて、前回の記事では、Railsチュートリアルの Sample App をコンテナ化しました。
しかし、実際の開発にあたり、煩わしい部分や便利になるところがありますので、改善していきましょう。

---

- 開発環境
    - macOS Mojave: 10.14.6
    - Docker Engine: 19.03.5
    - Docker Compose: 1.25.4
- コンテナ環境
    - Alpine Linux: 3.11.3
    - Ruby: 2.4.9
    - Rails: 5.1.2
- 参考: 学習情報 URL
    - [Get Docker \| Docker Documentation | 公式](https://docs.docker.com/get-docker/)
    - [Docker ドキュメント日本語化プロジェクト — Docker\-docs\-ja 17\.06\.Beta ドキュメント](http://docs.docker.jp/index.html)

---

# 【本文】
## □ 事前準備

- 前回の記事と同じファイル及びディレクトリ構成です。

```dockerfile:docker/Dockerfile
FROM ruby:2.4.9-alpine3.11

ENV LANG C.UTF-8 \
    TZ Asia/Tokyo

ENV BUILD_PACKAGES="build-base" \
    DB_PACKAGES="sqlite-dev postgresql-dev" \
    RAILS_PACKAGES="tzdata nodejs imagemagick" \
    FAVORITE_PACKAGES="less"

RUN apk update && \
    apk upgrade && \
    apk --update --no-cache add \
        ${BUILD_PACKAGES} \
        ${DB_PACKAGES} \
        ${RAILS_PACKAGES} \
        ${FAVORITE_PACKAGES}

WORKDIR /app

COPY Gemfile \
     Gemfile.lock \
     /app/

RUN bundle install --jobs=4

# https://github.com/bundler/bundler/issues/6154
ENV BUNDLE_GEMFILE='/app/Gemfile'
```

```yml:docker-compose.yml
version: '3'
services:
  app:
    build:
      context: .
      dockerfile: ./docker/Dockerfile
    ports:
      - 3000:3000
    command: bundle exec rails s -p 3000 -b 0.0.0.0
    volumes:
      - ./:/app:cached
    stdin_open: true
    tty: true
```

## □ Gem の永続化
### ■ 現状の問題点
- 結論から言うと、現状では Gemfile を編集する度に`docker-compose build`をする必要があリます。つまり、大量の Gem を初めから全て`bundle install`することになる煩わしい状態なのです。
- 理由としては、コンテナの構造によるものであり、この問題点を体験したい場合、事前準備の条件で以下の通り動かしてみると良いでしょう。[^1]

```bash:bash(省略可)
 # (1) コンテナのイメージ構築(ここのイメージがコンテナ構築時のデフォルト設定)
 $ docker-compose build

 # (2) コンテナを構築及び起動
 $ docker-compose up -d

 # (3) Gemfile に Gem を追加
  ...
  group :development, :test do
    ...
    gem 'pry-rails'
    gem 'pry-byebug'
  end
  ...

 # (4) 起動済のコンテナ内で bundle install
 $ docker-compose exec app bundle install

 # (5) 追加した Gem が確認できる( /usr/local/bundle/ にインストールされている)
 $ docker-compose exec app gem list

 # (6) コンテナを削除
 $ docker-compose down

 # (7) コンテナを起動すると...
 $ docker-compose up
 > Starting sample_app_app_1 ... done
 > Attaching to sample_app_app_1
 > app_1  | Could not find pry-byebug-3.4.3 in any of the sources
 > app_1  | Run `bundle install` to install missing gems.
 > sample_app_app_1 exited with code 7

 # (8) 結果
 - pry-byebug が見つからないため、bundle install が必要な状態です。
 - つまり、コンテナの状態が(1)に戻ってきていることがわかります。
```

[^1]: `docker-compose stop`と`start`なら作業状態を継続できることから、Gem もインストール済で作業を継続できる。

### ■ 解決策
- 前述の通り原因は、`起動したコンテナ内で bundle install`するだけで、設計図となるコンテナのイメージ自体に反映されていないことです。
- 解決策としては、Gem をコンテナ外で管理することです。コンテナを削除しても、Gem が存在する場所に影響しません。ここでは、コンテナ外に Gem の保管場所( = volume )を作成して、コンテナ内の Gem を保存する場所と繋げます（ = mount ）。

- 参考URL
    - [Dockerのまとめ \- コンテナとボリューム編 \- Qiita](https://qiita.com/kompiro/items/7474b2ca6efeeb0df80f)
    - [Moby で LinuxKit を起動してみる \- Qiita](https://qiita.com/pottava/items/3e64cf0476d4cfa93169)


**○ 1： docker-compose.yml に data volume を追加**

```diff:docker-compose.yml
version: '3'
services:
  datastore:
    image: busybox
+    volumes:
+      - bundle_install:/usr/local/bundle
  app:
    build:
      context: .
      dockerfile: ./docker/Dockerfile
    ports:
      - 3000:3000
    command: bundle exec rails s -p 3000 -b 0.0.0.0
    volumes:
+      - bundle_install:/usr/local/bundle
      - ./:/app:cached
    stdin_open: true
    tty: true
+ volumes:
+   bundle_install:
```

**○ 2： 再度 bundle install**

```bash:bash
 # 前の手順で、Gemfile に Gem を追加していましたら、該当箇所を一度コメントアウトしてください。

 # (1) コンテナのイメージ構築
 $ docker-compose build

 # (2) コンテナを構築及び起動
 $ docker-compose up -d

 # (3) Gemfile に Gem を追加
  ...
  group :development, :test do
    ...
    gem 'pry-rails'
    gem 'pry-byebug'
  end
  ...

 # (4) 起動済のコンテナ内で bundle install
 $ docker-compose exec app bundle install

 # (5) コンテナを削除
 $ docker-compose down

 # (6) コンテナを起動すると正常に動作しています。
 $ docker-compose up
```

**○ 3： どこに保存されたのか？**

- では、データの保管先を確認してみましょう。
- 追加した volume は、通常のディレクトリに存在せず、別の場所に存在しています。なお、ディレクトリ内を探検してみると、コンテナも保存していることがわかります。

```bash:bash(省略可)
 # Moby VM にアクセス
 $ screen ~/Library/Containers/com.docker.docker/Data/vms/0/tty -s

 # 〜VM内操作へ移行〜

 # 以下のコマンドにより、`sample_app_bundle_install/`の存在が確認できる。
 $ ls /var/lib/docker/volumes/

 # この Volume は、data_store で定義した通り、`/usr/local/bundle`を保存する。
 $ ls /var/lib/docker/volumes/sample_app_bundle_install/_data
 
 # 終了時
 $ `controll` + `A` + `K`
 $ yes
```

**○ 4： Gem の永続化完了**

- ここまでで、Gem の永続化が完了したため、次はデータベースをコンテナ化します。
- もちろん、データベースをコンテナ化したとしても、コンテナの削除時にデータベースに保存したデータは失われてしまいます。そのため、Gem の永続化と同様にデータベースも併せて永続化していきます。

## □ データベースのコンテナ化
### ■ 現状の問題点

- Sample App における本番環境のデータベースは PostgreSQL で、開発環境は SQLite を使用しています。しかし、基本的に開発環境と本番環境は、同一化が求められます。「開発環境では動いたのに本番で動かない（絶望）」ということ自体があってはなりません。
- しかし、これを実践しようとしてローカル環境で開発するとき、逐一 PostgreSQL を起動したり、プロジェクトによって異なるバージョンを用意する必要があったり、煩わしい部分があるため、コンテナ化します。

- 参考URL
    - [Docker で作る postgres 環境](https://crudzoo.com/blog/docker-postgres)

### ■ 解決策

**○ 1： docker-compose.yml の編集**

```diff:docker-compose.yml
version: '3'
services:
  datastore:
    image: busybox
    volumes:
      - bundle_install:/usr/local/bundle
+       - db_data:/var/lib/postgresql/data
+   db:
+     image: postgres:12.2-alpine
+     volumes:
+       - db_data:/var/lib/postgresql/data
+     ports:
+       - 5432:5432
+     environment:
+       POSTGRES_USER: root
+       POSTGRES_PASSWORD: pass
  app:
    build:
      context: .
      dockerfile: ./docker/Dockerfile
    ports:
      - 3000:3000
    command: bundle exec rails s -p 3000 -b 0.0.0.0
    volumes:
      - bundle_install:/usr/local/bundle
      - ./:/app:cached
+     depends_on:
+       - db
+     environment:
+       APP_DATABASE_HOST: db
+       APP_DATABASE_USERNAME: root
+       APP_DATABASE_PASSWORD: pass
    stdin_open: true
    tty: true
volumes:
  bundle_install:
+   db_data:
```

**○ 2： config/database.yml の編集**

- Rails の DB 設定は、SQLite のままであるため、PostgreSQL に変更します。
- なお、ここでは差分表示していませんので、全て置換してください。

```yml:config/database.yml
default: &default
  adapter: postgresql
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  host: <%= ENV.fetch("APP_DATABASE_HOST") { '127.0.0.1' } %>
  port: <%= ENV.fetch("APP_DATABASE_PORT") { '5432' } %>
  username: <%= ENV.fetch("APP_DATABASE_USERNAME") { 'root' } %>
  password: <%= ENV.fetch("APP_DATABASE_PASSWORD") { 'pass' } %>

development:
  <<: *default
  database: sample_app_development

test:
  <<: *default
  database: sample_app_test

production:
  <<: *default
  database: sample_app_production
```

**○ 3： Gemfile の編集**

- 開発環境を PostgreSQL に変更するため、現在の Gemfile の構成もついでに変更しておきます。

```diff:Gemfile
...

group :development, :test do
-   gem 'sqlite3', '1.3.13'
  gem 'byebug',  '9.0.6', platform: :mri
  gem 'pry-rails'
  gem 'pry-byebug'
end
...

- group :production do
-   gem 'pg', '0.18.4'
- end
+ gem 'pg', '0.18.4'
...
```

**○ 4： DB コンテナの起動**

```bash:bash
 # コンテナと volume を削除してから起動
 $ docker-compose down -v
 $ docker-compose up -d
```

**○ 5： データベースの作成**

```bash:bash
 $ docker-compose exec app rails db:create
 $ docker-compose exec app rails db:migrate
 $ docker-compose exec app rails db:seed
```

**○ 6： DB のコンテナ化完了**

- これで、DB のコンテナ化が完了です。
- DBコンテナに入って実際に確認したい場合は、次の通り操作してみてください。

```bash:bash(省略可)
 # DB コンテナに入る
 $ docker-compose exec db ash

 # PostgreSQL に登録されている Sample App のデータベースに接続
 $ psql -U root -d sample_app_development

 # テーブル生成の確認
 sample_app_development=$ \dt
 sample_app_development=$ exit
```


## □ メールサーバのコンテナ化
### ■ 現状の問題点
- 特にありませんが、一つのコマンド打つだけで見れるのですから、あったら便利ですよね。
- また、メールのデータも永続化できますが、ここで残すメリットを感じられなかったため、実装していません。永続化したい方は、参考URL等を確認しつつ実装してみてください。要領は前項までと同じです。

- 参考URL
    - [Docker \+ Rails \+ Mailhogでメールを送信せずにブラウザで確認できる開発環境を作る \- ココナラよもやまブログ](https://yomoyamablog.coconala.co.jp/entry/2018/08/03_2)


### ■ 解決策

**○ 1： docker-compose.yml の編集**

```diff:docker-compose.yml
version: '3'
services:
  datastore:
    image: busybox
    volumes:
      - bundle_install:/usr/local/bundle
      - db_data:/var/lib/postgresql/data
  db:
    image: postgres:12.2-alpine
    volumes:
      - db_data:/var/lib/postgresql/data
    ports:
      - 5432:5432
    environment:
      POSTGRES_USER: root
      POSTGRES_PASSWORD: pass
  app:
    build:
      context: .
      dockerfile: ./docker/Dockerfile
    ports:
      - 3000:3000
    command: bundle exec rails s -p 3000 -b 0.0.0.0
    volumes:
      - bundle_install:/usr/local/bundle
      - ./:/app:cached
    depends_on:
      - db
    environment:
      APP_DATABASE_HOST: db
      APP_DATABASE_USERNAME: root
      APP_DATABASE_PASSWORD: pass
    stdin_open: true
    tty: true
+   smtp:
+     image: mailhog/mailhog
+     ports:
+       - '8025:8025'
volumes:
  bundle_install:
  db_data:
```

**○ 2： 開発環境のメール設定を編集**

```diff:config/environments/development.rb
...
  # Don't care if the mailer can't send.
  config.action_mailer.raise_delivery_errors = true
-   config.action_mailer.delivery_method = :test
-   host = 'railstutorial-yasulab.c9users.io'
-   config.action_mailer.default_url_options = { host: host, protocol: 'https' }
+  config.action_mailer.delivery_method = :smtp
+  config.action_mailer.smtp_settings = {
+    address: "smtp",
+    port: "1025",
+  }
+  config.action_mailer.default_url_options = { host: "localhost:3000" }
...
```

**○ 3： メールサーバのコンテナ化完了**

- これで、メール確認が容易になりました。試しに SignUp してみるとアクティベーションメールが以下のURLに届きます。
- http://localhost:8025


## □ 特記事項
### ■ 注意事項
- 今のところ特になし


### ■ 使用イメージ、ライブラリ
- [ruby \- Docker Hub](https://hub.docker.com/_/ruby)
- [busybox \- Docker Hub](https://hub.docker.com/_/busybox)
- [postgres \- Docker Hub](https://hub.docker.com/_/postgres)
- [mailhog/MailHog: Web and API based SMTP testing](https://github.com/mailhog/MailHog)
- [mysql \- Docker Hub](https://hub.docker.com/_/mysql)

## □ おまけ: DB を MySQL にしたい場合

**○ 1： Dockerfile の編集**

```diff:docker/Dockerfile
...
ENV BUILD_PACKAGES="build-base" \
-     DB_PACKAGES="postgresql-dev" \
+     DB_PACKAGES="mysql-client mysql-dev" \
    RAILS_PACKAGES="tzdata nodejs imagemagick" \
    FAVORITE_PACKAGES="less"
...
```

**○ 2： docker-compose.yml の編集**

```diff:docker-compose.yml
...

+   db:
+     image: mysql:5.7
+     volumes:
+       - db_data:/var/lib/mysql
+     ports:
+       - 3306:3306
+     environment:
+       MYSQL_ROOT_PASSWORD: pass
-   db:
-     image: postgres:12.2-alpine
-     volumes:
-       - db_data:/var/lib/postgresql/data
-     ports:
-       - 5432:5432
-     environment:
-       POSTGRES_USER: root
-       POSTGRES_PASSWORD: pass
...
```

**○ 3： config/database.yml の編集**

```yml:config/database.yml
default: &default
  adapter: mysql2
  charset: utf8mb4
  collation: utf8mb4_bin
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  host: <%= ENV.fetch("APP_DATABASE_HOST") { '127.0.0.1' } %>
  port: <%= ENV.fetch("APP_DATABASE_PORT") { '3306' } %>
  username: <%= ENV.fetch("APP_DATABASE_USERNAME") { 'root' } %>
  password: <%= ENV.fetch("APP_DATABASE_PASSWORD") { 'pass' } %>

development:
  <<: *default
  database: sample_app_development

test:
  <<: *default
  database: sample_app_test

production:
  <<: *default
  database: sample_app_production
```

**○ 4： Gemfile の編集**

```diff:Gemfile
...

-  gem 'pg', '0.18.4'
+  gem 'mysql2', '~> 0.4.4'
...
```

- 参考URL
    - MySQL の charset 等について、上記の通り設定しない場合は以下の問題が発生するため、utf8mb4 を設定しておく( Rails のデフォルトだと utf8 になる )。
        - [寿司ビール問題①　初心者→中級者へのSTEP20/25 \- Qiita](https://qiita.com/kamohicokamo/items/3cc05f63a90148525caf)
        - [MySQLのencodingをutf8からutfmb4に変更して寿司ビール問題に対応する](https://techracho.bpsinc.jp/hachi8833/2016_08_25/25044)
    - Rail5.1 だと、mysql2 のバージョンを指定する必要がある。
        - [mysql2とRailsの組み合わせでエラーが発生する](https://www.tmp1024.com/articles/rails-mysql2-version-error)