チームで共有するための『Rails 6 x MySQL 8』Docker環境構築手順

今回はRails 6とMySQL 8を組み合わせたWebアプリケーションのDocker環境を構築する手順について紹介します。

Rails 6からwebpackerが標準でインストールされるようになったり、MySQL 8からユーザー認証の方式が変わったりと環境構築でつまる部分がいろいろとあったため参考になればと思います。

複数人でもスムーズに開発ができるようにするため**リモートリポジトリからcloneしてきたらdocker-compose upするだけでアプリケーションが立ち上がる**という環境をゴールにします。

各種バージョンは以下の通りです。

- Ruby on Rails: 6.0.3.2
- Ruby: 2.7.1
- MySQL: 8.0.21

実行環境はDocker Desktop for Mac（バージョン 2.3.0.4）を利用しています。

## Railsアプリケーションの準備

ディレクトリの作成・移動をします。
今回作成するRailsアプリケーション名は`sample_app`とします。

```bash
$ mkdir sample_app && cd $_
```

rubyイメージを利用してローカル環境にGemfileを作成します。
`-v`はバインドマウント（ホストとコンテナのディレクトリの同期）のオプションです。

ホストのカレントディレクトリとコンテナのワークディレクトリを同期させることで、コンテナ上で作成されるファイルをホストに配置します。

```bash
$ docker run --rm -v `pwd`:/sample_app -w /sample_app ruby:2.7.1 bundle init
```

作成されたGemfileの`gem "rails"`の部分をアンコメントし、railsのバージョンを指定します。


```diff:Gemfile
# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
- # gem "rails"
+ gem "rails", '~> 6.0.3.2'
```

`rails new`を実行するDocker環境を構築するためDockerfileを作成します。

Rails 6ではアプリケーションを作成する際に`rails webpacker:install`も実行されるので**yarnのインストールを忘れずにしましょう。**
今回は`npm`を利用して`yarn`をインストールします。


```docker:Dockerfile
FROM ruby:2.7.1

RUN apt-get update -qq && \
    apt-get install -y nodejs \
                       npm && \
    npm install -g yarn

# 作業ディレクトリを/sample_appに指定
WORKDIR /sample_app

# ローカルのGemfileをDokcerにコピー
COPY Gemfile* /sample_app/

# /sample_appディレクトリ上でbundle install
RUN bundle install
```

Dockerfileをビルドして作成されたイメージを利用してコンテナを起動し、コンテナ上で`rails new`をします。

```bash
$ docker build -t sample_app .

$ docker run --rm -v `pwd`:/sample_app sample_app rails new . –skip-bundle --database=mysql
```

`docker-compose.yml`を作成します。

今回は説明を簡略化するため、rootユーザーでMySQLに接続しています。
一般ユーザーで接続をする場合はMySQLのイメージに対して以下の環境変数を設定する必要があります。


| 環境変数 | 内容 |
|:--|:--|
| MYSQL_USER   |ユーザー名   |
| MYSQL_PASSWORD   | ユーザーパスワード  |


- 参考: <a href="https://nishinatoshiharu.com/dockerdb-user-accesses/" target="_blank">【Docker】Railsで理解する、複数のMySQLデータベースに一般ユーザーで接続するための環境構築手順</a>



データベースの情報は`mysql_data`という名前付きボリュームを作成して永続化します。

```yaml:docker-compose.yml

version: '3'
services:
  web: # Ruby on Railsが起動するコンテナ
    build: .
    ports:
      - '3000:3000' # localhostの3000ポートでアクセスできるようにする
    volumes:
      - .:/sample_app # アプリケーションファイルの同期
    depends_on:
      - db
    command: ["rails", "server", "-b", "0.0.0.0"]
  db: # MySQLが起動するコンテナ
    image: mysql:8.0.21
    volumes:
      - mysql_data:/var/lib/mysql # データの永続化
    command: --default-authentication-plugin=mysql_native_password # 認証方式を8系以前のものにする。
    environment:
      MYSQL_ROOT_PASSWORD: 'pass'
      MYSQL_DATABASE: 'sample_app_development'
volumes:
  mysql_data: # データボリュームの登録
```

- 参考: <a href="https://nishinatoshiharu.com/docker-volume-tutorial/" target="_blank">Dockerのデータを永続化！Data Volume（データボリューム）の理解から始める環境構築入門</a>

上記の`docker-compose.yml`の補足説明をします。

MySQLのDockerイメージでは`MYSQL_DATABASE`に設定された名前のデータベースを作成してくれます。
Ruby on Railsのdevelopment環境では`[アプリケーション名]_development`というデータベースを利用するため、`sample_app_development`を`MYSQL_DATABASE`に登録しています。
これで`rails db:create`を実行しなくてもdevelopment環境のデータベースを用意できます。

MySQL 8からは認証方式が`mysql_native_password`から`caching_sha2_password`に変更されました。
MySQL 8標準の`caching_sha2_password`の認証方式だとデータベースへ接続できず、Railsアプリケーション起動時に以下のようなエラーメッセージが表示されてしまいます。

```bash

Mysql2::Error::ConnectionError

Plugin caching_sha2_password could not be loaded: /usr/lib/x86_64-linux-gnu/mariadb19/plugin/caching_sha2_password.so: cannot open shared object file: No such file or directory
```

![スクリーンショット 2020-09-15 9.15.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140792/f17d54bb-7e32-5ae1-1bae-d1361eb5e29d.png)

そこで、`--default-authentication-plugin=mysql_native_password`で以前の`mysql_native_password`の認証方式を利用するようにしています。

次にRuby on Railsのデータベース接続設定を行います。

```yaml:config/database.yml

default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password: pass # (今回はrootなので)MYSQL_ROOT_PASSWORDと一致させる
  host: db # データベースのコンテナ名を設定する
```

### Railsアプリケーションの起動確認

Ruby on Rails 6とMySQL 8を組み合わせたDocker環境ができあがったので起動をしてみます。

```bash

$ docker-compose up
```

`localhost:3000`にアクセスして以下の画面が表示されればOKです。

![スクリーンショット 2020-09-15 9.23.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140792/4bd81290-1364-c699-12cf-c1362ac67819.png)


データベースのデータの永続化についても確認をしてみます。
例として`scaffold`で`event`に関する機能を作成します。

```bash

$ docker-compose exec web rails g scaffold event title:string
$ docker-compose exec web rails db:migrate
```

`localhost:3000/events/new`にアクセスし、例として『サンプルイベント』というレコードを登録してみます。

<p id="ezwEBNK">
<img class="alignnone size-full wp-image-7320 " src="https://nishinatoshiharu.com/wp-content/uploads/2020/09/img_5f60b34e96986.png" alt="" /></p>


コンテナを削除・起動してもデータが残っていればOKです。

```bash
$ docker-compose down
$ docker-compose up

$ docker-compose exec web rails c
> Event.last.title
=> "サンプルイベント"
```


## テスト環境用のデータベースの作成

`MYSQL_DATABASE`を利用してdevelopment環境のデータベースを作成することで`db:create`を実行することなくRailsアプリケーションを起動できるようにしました。

しかし、**テストコードで利用するtest環境のデータベース(`[アプリケーション名]_test`)が作られていないためこれだけでは開発環境としては不十分**です。

MySQLのDockerイメージでは`/docker-entrypoint-initdb.d`にスクリプト(`.sql`、`.sh`、`.sql.gz`)を配置しておくとコンテナ起動時に実行してくれるという機能があります。[^docker-entrypoint-initdb]
この機能を活用してtest環境用のデータベースを作成します。

[^docker-entrypoint-initdb]: <a href="https://hub.docker.com/_/mysql" target="_blank">Docker Hub『MySQL: Initializing a fresh instance』</a>


**スクリプトはアルファベット順で実行される**ため、スクリプト間に依存関係がある場合はファイル名も意識してつけるようにしておきましょう。

```bash
$ mkdir docker-entrypoint-initdb.d && cd $_
$ vim 00_create.sql
```

```sql:00_create.sql
-- test環境用のデータベースを作成する
CREATE DATABASE sample_app_test;
```

なお、一般ユーザーのデータベースアクセス権は`MYSQL_DATABASE`に設定したデータベースのみとなっています。
ですので、**一般ユーザーでデータベースに接続する場合はデータベースの作成だけでなく、以下のようなアクセス権の付与も実行する必要があります。**

```sql:01_grant.sql
-- webappという名前の一般ユーザーを利用する場合
GRANT ALL ON `sample_app_test`.* TO webapp@'%';
```

- 参考: <a href="https://nishinatoshiharu.com/dockerdb-user-accesses/" target="_blank">【Docker】Railsで理解する、複数のMySQLデータベースに一般ユーザーで接続するための環境構築手順</a>

作成したスクリプトをコンテナ上で読み取れるようにするためバインドマウントを追加します。


```diff:docker-compose.yml
version: '3'
services:
  web:
    build: .
    ports:
      - '3000:3000'
    volumes:
      - .:/sample_app
    depends_on:
      - db
    command: ["rails", "server", "-b", "0.0.0.0"]
  db:
    image: mysql:8.0.21
    volumes:
      - mysql_data:/var/lib/mysql
+     - ./docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
    command: --default-authentication-plugin=mysql_native_password
    environment:
      MYSQL_ROOT_PASSWORD: 'pass'
      MYSQL_DATABASE: 'sample_app_development'
volumes:
  mysql_data:
```

### データベースが自動作成されることを確認

dbコンテナを削除・起動し、自動でデータベースが作られるか確認をしてみます。
`[アプリケーション名]_development`と`[アプリケーション名]_test`のデータベースが存在していればOKです。

```bash
# データベースの永続化情報も削除して0からデータベースを作成する
$ docker-compose down --volumes

$ docker-compose up

$ docker-compose exec db mysql -uroot -ppass -D sample_app_development

mysql> show databases;
+------------------------+
| Database               |
+------------------------+
| information_schema     |
| mysql                  |
| performance_schema     |
| sample_app_development |
| sample_app_test        | ← test用のdbが作成されている
| sys                    |
+------------------------+
6 rows in set (0.00 sec)

```

Railsアプリケーションからも正常に接続できます。

```bash
$ docker-compose exec web rails c

> ENV['RAILS_ENV']
=> "development

$ docker-compose exec web rails c -e test

> ENV['RAILS_ENV']
=> "test"
```

## 『clone → docker-compose up』だけで環境が立ち上がるようにする

DockerでRailsの開発環境を構築する方法としてよく見かけるのが以下のようなパターンです。

```bash
# コンテナを立ち上げる
$ docker-compose up

# データベースの作成
$ docker-compose exec web rails db:create

# テーブルのマイグレーション
$ docker-compose exec web rails db:migrate
```

Railsアプリケーションとデータベースをdocker-composeで連携させ、アプリケーションで利用するデータベースはコンテナ上で構築するという方法です。

上記の方法でも問題はないのですが、この場合だと**初回起動時にデータベースとテーブルの作成をコンテナ上で手動実行する手間がかかります。**

複数人でDocker環境を共有する時のことを考えると、リモートリポジトリからアプリケーションファイルをcloneしてきたら`docker-compose up`をするだけで環境が立ち上がるのが理想です。

共有メンバーに対して「初回起動時はデータベースの作成をコンテナ上で実施してね」とわざわざ伝えないといけない状況はできれば避けたいです。

ここからはリモートリポジトリからcloneしたら`docker-compose up`するだけでRailsアプリケーションが起動できるようにするための設定を行います。

### yarn installをコンテナ起動時に実行する

開発環境ではバインドマウト（ホストとコンテナのディレクトリの同期）を利用したソースコードの同期がよく利用されます。

リモートリポジトリからcloneしてきたアプリケーションファイルは`package.json`はありますが、`node_modules`ディレクトリはありません。
そのため、cloneしたあと`docker-compose up`をすると`node_modules`がない状態のディレクトリがバインドマウントされます。その結果`yarn install`の実行を促すエラーが発生し、アプリケーションが起動できません。

```bash

========================================
  Your Yarn packages are out of date!
  Please run `yarn install --check-files` to update.
========================================

To disable this check, please change `check_yarn_integrity`
to `false` in your webpacker config file (config/webpacker.yml).

yarn check v1.22.5
info Visit https://yarnpkg.com/en/docs/cli/check for documentation about this command.
```

これを防ぐにはコンテナ起動時に`yarn install`を実行する必要があります。
たとえば以下のようにすることでコンテナ起動時に`yarn install`を実行できます。

```diff:docker-compose.yml
version: '3'
services:
  web:
    build: .
+   command: ["./start.sh"]
-   command: ["rails", "server", "-b", "0.0.0.0"]
    ports:
      - '3000:3000'
    volumes:
      - .:/sample_app
    depends_on:
      - db
  db:
    image: mysql:8.0.21
    command: --default-authentication-plugin=mysql_native_password
    environment:
      MYSQL_ROOT_PASSWORD: 'pass'
      MYSQL_DATABASE: 'sample_app_development'
```


```sh:start.sh
#!/bin/bash -eu
yarn
rails server -b '0.0.0.0'
```

シェルのパーミッションを変更します。

```bash
$ chmod 755 start.sh
```

コンテナを起動する際のコマンドでシェルを呼び出し、シェルの中で`yarn install`を実行するようにしています。
`yarn install`実行後、`rails server`でRailsアプリケーションが起動するため先ほどのエラーが解消されます。


### マイグレーションを自動実行できるようにする
テーブルのマイグレーションも`yarn install`と同様、コンテナ起動時に呼び出されるシェルスクリプトで実行するようにします。


```diff:start.sh
#!/bin/bash -eu
yarn
+ rails db:migrate
rails server -b '0.0.0.0'
```

しかし、`docker-compose.yml`の`depends_on`を利用することでコンテナの起動順は制御できますが、コンテナの起動を待つということはできないため[^startup-order]、dbコンテナの起動準備が終わる前に`db:migrate`が実行されるとマイグレーションが正常に行われません。

[^startup-order]: <a href="https://docs.docker.jp/compose/startup-order.html" target="_blank">Docker Doc『Compose における起動順の制御』</a>

マイグレーションの自動化をするにはdbコンテナの起動を待ってから`db:migrate`が実行されるようにする必要があります。

データベースの準備を待つ方法にはいくつかありますが、今回は<a href="https://github.com/vishnubob/wait-for-it" target="_blank">wait-for-it</a>を利用する方法を紹介します。

`wait-for-it.sh`をカレントディレクトリに配置し、`docker-compose.yml`を以下のようにするとデータベースの起動を待ってからスクリプトが実行されます。


```diff:docker-compose.yml
version: '3'
services:
  web:
    build: .
-   command: [""./start.sh"]
+   command: ["./wait-for-it.sh", "db:3306", "--", "./start.sh"]
    ports:
      - '3000:3000'
    volumes:
      - .:/sample_app
    depends_on:
      - db
  db:
    image: mysql:8.0.21
    command: --default-authentication-plugin=mysql_native_password
    environment:
      MYSQL_ROOT_PASSWORD: 'pass'
      MYSQL_DATABASE: 'sample_app_development'
```

## まとめ
以上でRails 6 + MySQL 8のDocker環境の構築手順の紹介を終わります。

コンテナ起動時にシェルを実行することで、複数人でDocker環境を利用する場合でもスムーズに開発環境を構築できるようにしました。
なお、今回はコンテナ起動時にシェルを呼び出すことで`yarn install`や`db:migration`を確実に実行するようにしましたが、コンテナ起動時のコマンドの制御は<a href="https://github.com/progrium/entrykit" target="_blank">Entrykit</a>と呼ばれるツールでも行えます。

Entrykitについては<a href="https://nishinatoshiharu.com/use-entrykit-for-rails/" target="_blank">RailsのDocker環境にEntrykitを導入し、bundle installを自動実行させる方法</a>で紹介していますので、興味のある方はご覧になってください。

- 今回のまとめ
    - Rails 6からはyarnのインストールも事前に行う必要がある
    - MySQL 8の認証エラーが発生した場合は標準の認証に戻すことで解決する
    - コンテナ起動時にシェルを実行することでテーブル作成等の自動化が可能になる

## さいごに

Twitter(<a href="https://twitter.com/nishina555" target="_blank" rel="noopener">@nishina555</a>)やってます。フォローしてもらえるとうれしいです！

## 参考記事

- [Entrykit のすすめ](https://qiita.com/spesnova/items/bae6406bf69d2dc6f88b)
- [丁寧すぎるDocker-composeによるrails5 + MySQL on Dockerの環境構築(Docker for Mac)](https://qiita.com/azul915/items/5b7063cbc80192343fc0)
- [DockerでRuby on Railsの環境構築を行うためのステップ【Rails 6対応】](https://qiita.com/kodai_0122/items/795438d738386c2c1966)
- [DockerでのRuby on Rails環境構築を一つずつ詳解する](https://qiita.com/daichi41/items/dfea6195cbb7b24f3419)
- [Rails6の開発環境をDockerで構築しようとしたらドハマりした件](https://qiita.com/m-itoidcf/items/a1ad7b982402b33189b7)