Ruby 3.x, Rails 6.x, MySQL 8.x の Docker 環境構築。

# 概要

Docker と docker-compose を使い、アプリケーションサーバを Ruby 3.x, Ruby on Rails 6.x、DB サーバを MySQL 8.x でコンテナの構築するまでの手順となります。

なお、この記事ではセキュリティについての考慮は一切していません。

Windows 10 の WSL2 - Ubuntu 18.04 と Mac で確認していますが、後述する理由により Mac の方がお勧めです。

PostgreSQL の方が好みの方は、以下のページをご確認ください。
cf. [クィックスタート: Compose と Rails](https://docs.docker.jp/compose/rails.html)

## 前提

下記の環境が設定されていること。

### Windows 10

- WSL2 Ubuntu 18.04+
- Docker
- docker-compose
- MySQL Client（必要に応じて）

### Mac

- Docker
- docker-compose
- Docker Desktop for Mac
- MySQL Client（必要に応じて）

また、Docker のサービスが起動済みであること。

# 初期ファイル構成
以下のファイルから Rails プロジェクトを新規に作成します。

```console
.
├── docker
│   ├── app
│   │   ├── Dockerfile
│   │   └── entrypoint.sh
│   └── db
│       ├── Dockerfile
│       ├── conf.d
│       │   └── my.cnf
│       └── initdb.d
│           └── init.ddl.sql
├── scripts
│   └── wait-for-it.sh
├── docker-compose.yml
├── Gemfile
└── Gemfile.lock
```

- `wait-for-it.sh` は https://github.com/vishnubob/wait-for-it からいただきました。最初に `chmod +x scripts/wait-for-it.sh` で実行権限を付加しておいてください。

# 初期ファイル設定

## docker-compose.yml
注意点は app 側の `build: context:` で基準になるフォルダを root として、Dockerfile のパスを指定しているところです。
これは Dockerfile 内で Gemfile をコピーする必要があるのですが、`build: ./docker/app` としてしまうとフォルダを遡って Gemfile の操作ができないため、起点を root にしています。
Dockerfile が root にあれば関係ないのですが、今回は docker フォルダ下にしているため、このような対応となります。

また app の `command:` で先述の `wait-for-it.sh` を利用し、DB が起動するまで `rails server` を立ち上げないようにしています。

```docker-compose.yml
version: "3.3"

services:
  db:
    container_name: "db"
    build: ./docker/db
    restart: always
    tty: true
    environment:
      MYSQL_DATABASE: app_development
      MYSQL_USER: user
      MYSQL_PASSWORD: password
      MYSQL_ROOT_PASSWORD: password
      TZ: 'Asia/Tokyo'
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    ports:
      - "3306:3306"
    volumes:
      - ./docker/db/conf.d:/etc/mysql/conf.d
      - ./docker/db/initdb.d:/docker-entrypoint-initdb.d
    networks:
      - backend

  app:
    container_name: "app"
    build:
      context: ./
      dockerfile: ./docker/app/Dockerfile
    ports:
      - "3000:3000"
    environment:
      PORT: 3000
      BINDING: 0.0.0.0
    tty: true
    depends_on:
      - "db"
    command: ["./scripts/wait-for-it.sh", "db:3306", "--", "bundle", "exec", "rails", "s", "-p", "3000", "-b", "0.0.0.0"]
    volumes:
      - .:/app
    networks:
      - frontend
      - backend

networks:
  frontend:
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 192.168.10.0/24
  backend:
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 192.168.20.0/24
```

## app 用設定ファイル

### docker/app/Dockerfile

ここで Gemfile および Gemfile.lock をホスト（ローカル）からゲスト（コンテナ）にコピーしています。

```:docker/app/Dockerfile
FROM ruby:3.0

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

RUN apt-get update -qq && \
    apt-get install -y --no-install-recommends sudo curl apt-transport-https wget build-essential libpq-dev nodejs default-mysql-client

RUN 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 --no-install-recommends -y yarn

RUN apt-get clean && \
    rm -rf /var/lib/apt/lists/*

RUN mkdir /app
WORKDIR /app
COPY Gemfile /Gemfile
COPY Gemfile.lock /Gemfile.lock
RUN bundle install
COPY . /app

COPY docker/app/entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000
```

### docker/app/entrypoint.sh

こちらは以下のサイトの `entrypoint.sh` からいただきました。
cf. [Quickstart: Compose and Rails](https://docs.docker.com/compose/rails/)

```docker/app/entrypoint.sh
#!/bin/bash
set -e

# Remove a potentially pre-existing server.pid for Rails.
rm -f /myapp/tmp/pids/server.pid

# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"
```

## db 用設定ファイル

### docker/db/Dockerfile

```:docker/db/Dockerfile
FROM mysql:8.0

RUN apt-get update -qq && \
    apt-get install -y --no-install-recommends locales && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/* && \
    locale-gen ja_JP.UTF-8

RUN sed -i -E 's/# (ja_JP.UTF-8)/\1/' /etc/locale.gen && locale-gen

ENV LANG ja_JP.UTF-8
ENV TZ Asia/Tokyo
```

### docker/db/conf.d/my.cnf

my.cnf は文字コード指定が中心ですが、今回はシンプルに Rails の実行のみを考えているため、`default_authentication_plugin=mysql_native_password` で認証プラグインを変更しておきます。

```docker/db/conf.d/my.cnf
[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_bin
default-storage-engine=INNODB
explicit-defaults-for-timestamp=1
general-log=1
general-log-file=/var/log/mysql/mysqld.log

default_authentication_plugin=mysql_native_password

[mysqldump]
default-character-set=utf8mb4

[mysql]
default-character-set=utf8mb4

[client]
default-character-set=utf8mb4
```

### docker/db/initdb.d/init.ddl.sql

データベースを development 以外に test 用も作成する場合のファイルです。
実際は test 用データベースは `rake db:create` で作成されるはずですのでなくても問題ないと思われます。
コピー先の `/docker-entrypoint-initdb.d` フォルダではシェルの実行も可能なので、組み込み次第ではいろいろと対応できるようです。

```docker/db/initdb.d/init.ddl.sql
CREATE DATABASE IF NOT EXISTS `app_test`;
GRANT ALL ON app_test.* TO 'user'@'%';
```

## Gemfile

### Gemfile
初期は Rails のバージョン指定のみとなります。

```:Gemfile
source 'https://rubygems.org'
gem 'rails', '~>6'
```


### Gemfile.lock
初期状態は空ファイルとなります。

```Gemfile.lock
```

# 初期起動までの手順

## rails new
Docker のサービスが起動しているか確認の上、docker-compose.yml があるフォルダで `database` を MySQL に指定して `rails new` を実行します。

```console
$ docker-compose run app rails new . --force --database=mysql
```

問題なく実行が完了すると、実行したフォルダに Rails アプリのファイル群が作成されます。
このとき、Mac の場合は実行したユーザーの権限でファイルが作成されますが、WSL の場合は root 権限となり、そのままではファイルの更新が行えません。

cf. [【Docker】 WSL 2 を利用したコンテナー内開発で権限をどう設定するべきか](https://futureys.tokyo/how-permission-should-be-set-for-developing-inside-a-container-using-wsl-2/)

根本的な解決ではないとは思いますが、とりあえず以下のコマンドで権限を実行ユーザーに振り替えて対応することは可能です。
ただし、ここだけではなく、scaffold など rails のコマンドでファイルを作成・編集するごとに権限を書き換える必要があります。
（これが WSL よりも Mac をお勧めする理由です）

```console
$ sudo chown -R $USER:$USER .
```

この時点で、作成された config/database.yml を編集し、MySQL へのアクセス設定を変更します。

```config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: user
  password: password
  host: db
```

## docker-compose build, up

build でサービスを作成し、問題がなければ up でコンテナを起動します。
この際、`--no-cache` オプションを付けるのは、`bundle install` 実行時に gem ファイルを巧く取り込めない場合がある（らしい）ためです。

```console
$ docker-compose build --no-cache
$ docker-compose up -d
```

この時、db および app がほぼ同時に立ち上がりますが、docker-compose.yml で記述した通り、app の rails server は MySQL サーバとの接続が確立するまで実行されないようになっています。
それぞれのコンテナのログは `docker logs` で確認できるため、以下のように確認して下さい。

```console
$ docker logs app # アプリケーションサーバのログ
$ docker logs db # DB サーバのログ
```

実行に問題がなければ、ブラウザまたは curl などで `http://localhost:3000/` にアクセスすることで、いつもの Rails の初期画面が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1144449/bd89da72-ceed-15c6-9349-867c9acb9b34.png)

# scaffold 作成と実行の確認

scaffold で MVC を作成して動作が可能か確認します。


```console
$ docker-compose run app rails g scaffold user name:string email:string
```

WSL で操作している場合はファイル権限の変更をしてください。

```console
$ sudo chown -R $USER:$USER .
```

`db:migrate` でテーブルを作成します。

```console
$ docker-compose run app rails db:migrate
```

ブラウザで `http://localhost:3000/users` にアクセスすることで scaffold で作成した Rails 標準の UI が表示され、CRUD の一連の操作が可能なことが確認できます。

`http://localhost:3000/users/new`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1144449/a7db061c-6f44-8ff2-0a9d-cada8a8426dd.png)

`http://localhost:3000/users`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1144449/cbd2ad11-e721-0d9e-2073-c905d7f633c8.png)


実際の DB を確認したい場合は MySQL Client が入っていればコマンドで確認できます。
（設定を変えていない場合は、user アカウントのパスワードは `password` となります）

```console
$ mysql -u user -h 127.0.0.1 -D app_development -p
```

```sql
mysql> show create table users;
+-------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table                                                                                                                                                                                                                                                                                                              |
+-------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| users | CREATE TABLE `users` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `email` varchar(255) DEFAULT NULL,
  `created_at` datetime(6) NOT NULL,
  `updated_at` datetime(6) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci |
+-------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

mysql> select * from users;
+----+------+-----------------+----------------------------+----------------------------+
| id | name | email           | created_at                 | updated_at                 |
+----+------+-----------------+----------------------------+----------------------------+
|  1 | test | aaa             | 2021-02-25 15:56:02.123864 | 2021-02-25 15:56:02.123864 |
|  2 | test | aaa@example.com | 2021-02-25 16:38:33.311981 | 2021-02-25 16:38:33.311981 |
+----+------+-----------------+----------------------------+----------------------------+
2 rows in set (0.00 sec)
```

今回は以上となります。

# 参考資料

以下の記事、情報を参考にさせていただきました。
ありがとうございます。

- [Quickstart: Compose and Rails](https://docs.docker.com/compose/rails/)
- [クィックスタート: Compose と Rails](https://docs.docker.jp/compose/rails.html)
- [Docker Compose入門](https://knowledge.sakura.ad.jp/21387/)
- [Compose における起動順の制御](https://docs.docker.jp/compose/startup-order.html)