ちょっとwebアプリの検証をしたくてDockerCompose上にRails6 x MySQL8環境を作ろうと思ったのですが、案外ハマりどころが多かったのでメモに残しておくことにしました。
途中、たびたびpwdで作業ディレクトリを確認していますが、どこで実行しているのか迷わないようにするための解説的な意味合いが強いので、理解できている人は無視してください。

作業ディレクトリの作成〜下準備

作業ディレクトリを作って入る

メインになるプロジェクトのディレクトリと、ソースコードを放り込むディレクトリを作ります。

mkdir -p ~/docker-ruby/src
cd docker-ruby
touch Dockerfile
touch Gemfile

Gemfileを書く

最低限の内容をもったGemfileを書きます。
Gemfileは、Railsを入れるときに上書きされて内容が壊れてしまうので、使いまわせるようにDockefileと同じディレクトリにオリジナルを入れるようにしています。
また、Gemfileを配置したらtouchコマンドを使って空のGemfile.lockを作っておきます。

pwd #=> ~/docker-ruby
vi Gemfile
cp Gemfile src/Gemfile
touch src/Gemfile.lock

Gemfileの内容はこんな具合です。
バージョンを6系で固定する記述をしても良いと思いますが、メジャーバージョンが上がるのは年単位で先の話なので、今は無視しています。

source 'https://rubygems.org'
gem 'rails'

Dockerfileを書く

せっかくなのでRuby 2.7を使うことにしました。
ハマりどころとして、Rails6からはWebpackerが標準で入ってくるので、yarnを整える必要がああります。
そのため、yarnを導入するためのコマンドをズラズラと記載しています。
yarnの導入については、もっと良いやり方があるかもしれないです

FROM ruby:2.7

# シェルスクリプトとしてbashを利用
RUN rm /bin/sh && ln -s /bin/bash /bin/sh

# 必要なパッケージのインストール
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs

# yarnパッケージ管理ツールインストール
RUN export APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=1 && apt-get update && apt-get install -y curl apt-transport-https wget && \
    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 -y yarn

# 作業ディレクトリの作成と設定
RUN mkdir /app
ENV APP_ROOT /app
WORKDIR $APP_ROOT

# ローカルで作ったGemfileをコンテナ内の作業ディレクトリに配置する
ADD ./src/Gemfile $APP_ROOT/Gemfile
ADD ./src/Gemfile.lock $APP_ROOT/Gemfile.lock

# Gemfileのbundle install
RUN bundle install
ADD src $APP_ROOT

docker-compse.ymlを用意する

閑話休題。KubernetesとかRailsとかdoocker-composeとかを扱っていると一番多く読み書きするのがソースコードじゃなくてYAMLのファイルになりがちだと思いませんか?

さて、docker-compose.ymlファイルを作ります。

pwd #=> ~/docker-ruby
vi docker-compose.yml

データベースにはMySQL8の公式イメージをそのまま使うことにしました。
また、DB_USERDB_PASSという名称で環境変数をexportして、docker-compose.yml内に秘密にしておくべき情報を記録しないようにしています。(今回の例だとDB_USERはデータベース名としても使われるので、一旦root固定です)

export DB_USER="root"
export DB_PASS="db_password"

二つ目のハマりポイントがここにあって、Docker + MySQL8の環境だと、こちらのQiita記事にあるようにcaching_sha2_passwordの認証形式に対応できません。
先のQiita記事では設定ファイルをマウントさせていますが、できれば外側にファイルを用意したくないので、MySQLにコマンドラインオプションを渡してAuthentication Pluginを変更して運用するようにしています。

version: '3'
services:
  db:
    image: mysql:8.0
    environment:
      MYSQL_DATABASE: ${DB_USER}
      MYSQL_ROOT_PASSWORD: ${DB_PASS}
    ports:
      - "3306:3306"
    command: --default-authentication-plugin=mysql_native_password

  app:
    build: .
    command: rails s -p 3000 -b '0.0.0.0'
    volumes:
      - ./src:/app
    environment:
      - DB_USER=${DB_USER}
      - DB_PASS=${DB_PASS}
    ports:
      - "3000:3000"
    links:
      - db

これで大体の準備が完了しました。

環境のビルド〜データベース設定の修正

rails newして下地を作る

rails newします。このときにコンテナがビルドされるので、ちょっと時間がかかります。
どうでもいいですけど、nokogirisassc等のnative extensionの導入は、もっと高速になって欲しいと思っています。

docker-compose run app rails new . --force --database=mysql --skip-bundle

ここで、newするときに--skip-bundleが付いていると

Could not find gem 'mysql2 (>= 0.4.4)' in any of the gem sources listed in your Gemfile.
Run `bundle install` to install missing gems.

などとエラーが出てしまいますが、一旦無視して先に進みます。

database.ymlの編集

ビルドできたら、データベースの設定ファイルを編集します。
ちなみに、ファイルの編集コマンドにviを使っていますが、実際はVSCodeを使って作業しています。

pwd #=> ~/docker-ruby
vi src/config/database.yml

docker-compose.ymlでDBのユーザとパスワードを環境変数として定義しているので、database.ymlでもそれを使うようにします。
ということで、下記の要領でdefault部分の設定を書き換えます。
hostdocker-compose.ymlで指定したデータベースコンテナの名前を与えるのを忘れないようにします。)

default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: <%= ENV.fetch("DB_USER") { "root" } %>
  password: <%= ENV.fetch("DB_PASS") { "" } %>
  host: db

コンテナビルドをしなおして、webpackerを導入する

設定も終わったのでコンテナをビルドしなおしてから、webpackerのインストールを行います。
(webpackerのインストールも結構時間がかかるので、気長に待ちます。)

最後のハマりポイントがここで、webpackerのインストールを忘れるとappコンテナを起動しても、何かしようとした途端に落ちてしまいます。(単にdocker-compose upしただけだと普通に動いているように見える(UP状態に見える)のがたちの悪い所です)

pwd #=> ~/docker-ruby
docker-compose build
docker-compose run app rails webpacker:install

appコンテナのビルドに際して、下記のようにtzinfo-dataのエラーが出ますが、私と同じDocker for macを使っている環境ならば無視してしまって大丈夫だと思います。(エラーメッセージにある通り、x86-mingw32/x86-mswin32/x64-mingw32/javaの各環境で依存関係が生じるものです)

The dependency tzinfo-data (>= 0) will be unused by any of the platforms Bundler is installing for. Bundler is installing for ruby but the dependency is only for x86-mingw32, x86-mswin32, x64-mingw32, java. To add those platforms to the bundle, run `bundle lock --add-platform x86-mingw32 x86-mswin32 x64-mingw32 java`.

また、docker-composeコマンドはよく使うので、適当にエイリアスをつけておくと良いと思います。

DBをつくって動作確認する

Railsを通じてDBを作ります。ここまで来ればもう難しいことはないです。

pwd #=> ~/docker-ruby
docker-compose down # 一回すべてのコンテナを落とす
docker-compose up -d
docker-compose ps #=> コンテナがUpしていることを確認する
docker-compose exec app rails db:create

出来上がったらhttp://0.0.0.0:3000にアクセスして動作確認します。
Yay!ページが表示されたら動作確認完了です。
これでDockerComposeとRails6 x MySQL8を組み合わせた環境ができあがりました!

Rails6のYay!ページ

最終的なディレクトリの姿

最後に、参考程度に最終的なディレクトリの姿を掲載しておきます。
srcディレクトリ以下にrailsのコードがまとまって、それより上のディレクトリにコンテナを動かすためのファイル群がまとまっているという感じで整理できているのがわかると思います。

pwd #=> ~/docker-ruby
tree -L 2
.
├── Dockerfile
├── Gemfile
├── docker-compose.yml
└── src
    ├── Gemfile
    ├── Gemfile.lock
    ├── README.md
    ├── Rakefile
    ├── app
    ├── babel.config.js
    ├── bin
    ├── config
    ├── config.ru
    ├── db
    ├── lib
    ├── log
    ├── node_modules
    ├── package.json
    ├── postcss.config.js
    ├── public
    ├── storage
    ├── test
    ├── tmp
    ├── vendor
    └── yarn.lock