【Rails x MySQL x Docker】まとめ

## 環境

- M1 Mac
- lima(arch: x86_64)
- MySQL:8.0
- Ruby:3.0.3
- Rails:6.1.4.4

## 留意点

MySQLにはunixソケットまたはtcpで接続できるが、ソケットだとディレクトリの権限でエラーになったりして手間なので、tcp接続についてのみ説明する。

## mysql2

RubyでMySQLに接続するためには[mysql2](https://github.com/brianmario/mysql2)を使用する。このgemは`Rails new`コマンド時に`-d mysql`を付与することでもインストールできる。

mysql2で接続を行うにはいくつかの[パラメータ](https://github.com/brianmario/mysql2/blob/master/README.md#connection-options)を設定する必要がある。

> The underlying MySQL client library uses the `:host` parameter to determine the
type of connection to make, with special interpretation you should be aware of:

> 
* An empty value or `"localhost"` will attempt a local connection:
  * On Unix, connect to the default local socket path. (To set a custom socket
    path, use the `:socket` parameter).
  * On Windows, connect using a shared-memory connection, if enabled, or TCP.
* A value of `"."` on Windows specifies a named-pipe connection.
* An IPv4 or IPv6 address will result in a TCP connection.
* Any other value will be looked up as a hostname for a TCP connection.

tcpで接続を行うには`host`パラメーターにMySQLのホスト名を指定する必要がある。mysql2は`host`が正しく設定されていない場合はソケット通信を試みる。

Railsの場合、これらのパラメーターの指定は`config/database.yml`にて行う。

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

development:
  <<: *default
  database: <%= ENV["MYSQL_DEV_DATABASE"] %>

test:
  <<: *default
  database: <%= ENV["MYSQL_TEST_DATABASE"] %>

production:
  <<: *default
  database: <%= ENV["MYSQL_PRD_DATABASE"] %>
```

MySQLはクライアントサーバシステムなので、ユーザーとそのパスワードを設定する必要がある。YAMLのエイリアスとインジェクトを使用して各環境に`default`を差し込んでいる。また接続に必要な情報はすべて環境変数から読み込んでいる。

## Docker環境の構築

今回は以下のような構成にする。

```
.
├── config
│   └── database.yml
├── docker
│   ├── docker-compose.yml
│   ├── .env
│   ├── mysql_db
│   │   └── conf.d
│   │       └── my.cnf
│   └── rails_app
│       └── Dockerfile.dev
├── .env
├── Gemfile
```

MySQL Dockerコンテナには起動及び接続用のユーザー設定を行うため[`MYSQL_ROOT_PASSWORD`、`MYSQL_USER`、`MYSQL_PASSWORD`](https://github.com/docker-library/docs/tree/master/mysql#environment-variables)の3つの環境変数を設定する。

`./.env`にはこれらを含む接続に必要な環境変数を設定する。パスワードは`pwgen`などによって適当に生成する。

```env:./.env
MYSQL_ROOT_PASSWORD=Thaithi3xez7
MYSQL_USER=rails-app
MYSQL_PASSWORD=aix1Eequei8d
MYSQL_DEV_DATABASE=dev-db
MYSQL_TEST_DATABASE=test-db
MYSQL_PRD_DATABASE=prd-dev
```

Dockerコンテナにこの環境変数を読み込む。次にRailsコンテナ用の`Dockerfile`、`docker-compose.yml`を設定する。

```docker/.env
COMPOSE_PROJECT_NAME=sample_project
```

```Dockerfile:docker/rails_app/Dockerfile.dev
ARG NODE_VERSION=14
ARG RUBY_VERSION=3.0.3

FROM node:${NODE_VERSION} as node
FROM ruby:${RUBY_VERSION}

# Install node and yarn
# see https://hub.docker.com/_/node
COPY --from=node /opt/yarn-* /opt/yarn
COPY --from=node /usr/local/bin/node /usr/local/bin/node
COPY --from=node /usr/local/lib/node_modules/ /usr/local/lib/node_modules/

RUN ln -s /opt/yarn/bin/yarn /usr/local/bin/yarn \
    && ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm

ENV PATH $PATH:\
/usr/local/bin/yarn:\
/usr/local/bin/node

RUN apt-get update -qq

# set envrionment variables
ENV RAILS_ENV="development" \
    NODE_ENV="development" \
    LANG="C.UTF-8" \
    TZ="Asia/Tokyo"

# Install gems
WORKDIR /rails_app

COPY Gemfile \
    Gemfile.lock \
    /rails_app/

RUN gem update bundler \
    && bundle config set path .cache/bundle
RUN --mount=type=cache,uid=1000,target=/rails_app/.cache/bundle \
    bundle install \
    && mkdir vendor \
    && cp -ra .cache/bundle vendor/bundle \
    && bundle config set path vendor/bundle

# Install npm packages
COPY package.json \
    yarn.lock \
    /rails_app/
RUN --mount=type=cache,uid=1000,target=/rails_app/.cache/node_modules \
    yarn install --modules-folder .cache/node_modules \
    && cp -ra .cache/node_modules node_modules

# Copy codes
COPY . /rails_app/

# Precompile assets
# RUN --mount=type=cache,uid=1000,target=/rails_app/tmp/cache/assets \
#     bundle exec rails assets:precompile

CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]
```

```yaml:docker/docker-compose.yml
version: "3.9"
services: 
  mysql_db:
    image: mysql:8.0
    container_name: dev-db
    hostname: &mysql_host mysql_host
    env_file:
      - ../.env
    volumes:
      - ./mysql_db/conf.d:/etc/mysql/conf.d
      - db_data:/var/lib/mysql
  
  rails_app:
    build:  
      context: ../
      dockerfile: ./docker/rails_app/Dockerfile.dev
    container_name: dev-server
    env_file:
      - ../.env
    environment:
      MYSQL_HOST: *mysql_host
    stdin_open: true
    tty: true
    ports:
      - 3000:3000
    command: bundle exec puma -C config/puma.rb
    volumes:
      - ..:/rails_app:cached
    depends_on: 
      - mysql_db

volumes:
  db_data:
```

`docker/.env`にてプロジェクト名を設定している。docker-compose.ymlのディレクトリに`.env`を設置することで[環境変数を適用](https://docs.docker.com/compose/env-file/)できる。`COMPOSE_PROJECT_NAME`でプロジェクト名を指定している。

Dockerfileでは[multi-stage build](https://docs.docker.com/develop/develop-images/multistage-build/)及びBuildkitによる`build`の高速化を図っている。詳細は以下の記事を参照。

https://qiita.com/k_kind/items/836bc7ba2e33dc2ed3e7

肝心の`docker/docker-compose.yml`では`env_files`によって`./.env`を読み込んでいる。またdocker-composeではサービス名及びコンテナ名(この場合、サービス名は`mysql_db`。コンテナ名は`dev-db`)がホスト名になるが、`hostname`によって明示的にホスト名を指定している。さらにエイリアスで`MYSQL_HOST`を設定することで整合をとっている。

これを`docker-compsoe -f docker/docker-compose.yml up -d`で起動、`docker logs`でMySQLコンテナのサーバーの起動を確認した後、設定したユーザーに権限を適用する。

```bash:./grant_mysql_user.sh
#!/bin/bash

source ./.env
source ./docker/.env

query=`cat << EOF
GRANT ALL PRIVILEGES ON *.* TO '${MYSQL_USER}'@'%'; 
FLUSH PRIVILEGES;
EOF
`

export COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME}
docker-compose exec mysql_db mysql -u root -p${MYSQL_ROOT_PASSWORD} -e"${query}"
```

`COMPOSE_PROJECT_NAME`を設定することで`docker-compose.yml`のディレクトリ外でも`docker-compose`コマンドでコンテナを操作できる。

```bash:./attach_rails_container.sh
#!/bin/bash

source ./docker/.env
export COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME}

docker-compose exec rails_app bash
```

最後にrailsコンテナにアタッチした後、`bundle exec rails db:create`とすればデータベースが生成される。ここで正しく生成されれば接続が確立されている。