DockerでRails(7.1.2) x MySQLの環境を構築し、DockerでHerokuにデプロイする

## はじめに
#### 何者か
RUNTEQ Advent Calendar 2023の11日目を担当いたします。raytoです。
RUNTEQというプログラミングスクールに通い始めて2ヶ月が経過しました。

初めて技術記事を書きますのでご容赦ください、と言いつつ、誤った情報やより効率の良い方法などがあればぜひ教えていただきたいです。

#### 背景
今回は、DockerでRails+MySQLの環境を構築して、DockerでHerokuにデプロイする手順を紹介します。GitでHerokuにデプロイする記事は多くみられましたが、Dockerでデプロイする方法に関する記事は多くないと思ったので書いてみました。
今回ご紹介する以外にも色々な方法や設定内容が考えられると思いますが、これからDockerを使用してRailsアプリを開発しようと思っている方は、一例として参考ししていただければ幸いです。

#### 補足
・本手順ではRailsアプリの開発が行える最小限の構成に近いものが出来上がるので、利用したいCSSフレームワークなどがあれば別途設定してください。
・GitHub上でのRailsアプリの管理、CI/CD等に関しては言及していません。（いつかこの記事の続編として別途書きたいと思っています。）
・Herokuの利用には料金が発生しますのであらかじめご了承ください。

## 1. 前提
* GitHubアカウントが作成されていること
* Herokuアカウントが作成されていること
* ローカルにDocker Desktopがインストールされていること


## 2. 使用機器とソフトウェア
* MacBook Air(M1)
* Ruby(3.2.2)
* Ruby on Rails(7.1.2)
* MySQL(8.2.0)

## 3. Dockerによるデプロイとは？
構築手順に入る前に、簡単にDockerによるデプロイとは何か、またそのメリットは何かについて説明いたします。

#### GitによるデプロイとDockerによるデプロイ
Herokuでは、Gitを利用してアプリケーションをデプロイして利用する方法が一般的かと思いますが、その他にDockerによるデプロイをサポートしています。そして、Dockerによるデプロイには2通りあります。
今回は、Dockerによるデプロイのうち、コンテナレジストリ​を使用したデプロイの手順をご紹介します。

https://devcenter.heroku.com/ja/categories/deploying-with-git
> Heroku は、ポピュラーな VCS (バージョン管理システム) である Git でほとんどのアプリのデプロイを管理します。

https://devcenter.heroku.com/ja/categories/deploying-with-docker
> Heroku では、Docker によるアプリのデプロイの方法を 2 つ提供しています。
・Container Registry​ を使用すると、事前にビルドされた Docker イメージを Heroku にデプロイできます。
・heroku.yml を使用して Docker イメージをビルド​して、Heroku にデプロイできます。

#### Dockerによるデプロイ（コンテナレジストリを使用したデプロイ）とは？
端的にいうと、ローカルで作成したアプリケーションやDocker関連の設定ファイルを元に、DockerイメージをHeroku上のコンテナレジストリという保管場所に格納し、そのイメージを使ってHeroku上でコンテナを起動してアプリケーションを公開する方法です。

#### Dockerによるデプロイのメリット

まず前提として、Gitによるデプロイの方がシンプルでDockerについて詳細に把握せずとも利用でき手軽なので、この点はGitによるデプロイのメリットかと思います。しかし、この場合には本番環境における環境設定は、すべて本番環境で行うことになります。
一方で、Dockerによるデプロイであれば、Dockerの設定ファイル上で環境設定を行うことができるため、「環境の一貫性を保ちやすい」、「環境設定の情報を管理しやすい」といったメリットがあると理解しています。
（残念ながら、Herokuのドキュメントにはメリット等の記載を見つけられませんでした。）

なお、AWSのECSやGoogle CloudのGKEなどのサービスは、Dockerによるデプロイによってアプリケーションを公開するサービスであり、より大規模なクラウドサービスにおいてはGitによるデプロイよりもこちらの方が主流のように思われます。

https://aws.amazon.com/jp/ecs/

https://cloud.google.com/kubernetes-engine?hl=ja

## 4. ローカル環境（development, test）の構築
それではここから実際の手順を進めていきます。まずはローカル環境の構築から行います。

### 4-1. DockerでRailsとMySQLの環境を構築するための準備
作業用のディレクトリや環境構築で使用するファイルを作成します。


#### Railsアプリ用のディレクトリを作成して移動
```shell:terminal
# 作業用ディレクトリに移動し、Railsアプリ用のディレクトリを作成して移動
cd <作業用のディレクトリ>
mkdir rails_app
cd rails_app

# 必要なファイルを作成
touch Dockerfile docker-compose.yml Gemfile Gemfile.lock start.sh
```
※「rails_app」は任意の名前でOKです。


#### Dokerfileを編集
```Dockerfile:Dockerfile
FROM --platform=linux/amd64 ruby:3.2.2

RUN apt-get update -qq && apt-get install -y build-essential default-mysql-client

ENV app_path /rails_app
RUN mkdir ${app_path}
WORKDIR ${app_path}

COPY ./Gemfile ${app_path}/Gemfile
COPY ./Gemfile.lock ${app_path}/Gemfile.lock

RUN bundle install
COPY . ${app_path}

COPY ./start.sh /start.sh
RUN chmod 744 /start.sh
CMD ["sh", "/start.sh"]
```

<details>
<summary>「--platform=linux/amd64」について</summary>
M1 MacなどのApple Silicon(CPUアーキテクチャがarm64)のPCの場合、DockerコンテナをHerokuにデプロイしようとすると以下のエラーが発生します。

```
Run `docker build --platform linux/amd64` to build the image to run on x86_64 architecture.
 ▸    Error: docker push exited with Error: 1
```
どうやら、デフォルトではPCのCPUアークテクチャに合わせてarm64用のイメージを取得して使用してしまうようです。明示的にamd64のイメージを指定して取得することでエラーを回避しています。


</details>

#### docker-compose.ymlを編集
```yml:docker-compose.yml
services:
  db:
    image: mysql:8.2.0
    platform: linux/amd64
    environment:
      MYSQL_ROOT_PASSWORD: password
    volumes:
      - mysql_data:/var/lib/mysql
    ports:
      - '3307:3306'
    command: --default-authentication-plugin=mysql_native_password
  web:
    build: .
    command: bash -c "rm -f tmp/pids/server.pid && bundle check || bundle install && rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/rails_app
    ports:
      - 3000:3000
    stdin_open: true
    tty: true
    depends_on:
      - db
volumes:
  mysql_data:
```

<details>
<summary>「MYSQL_ROOT_PASSWORD: password」とパスワードを記載してしまって問題ないの？</summary>
このパスワードはローカル環境（development, test）で使用するものであり、本番環境（production）では別のパスワードを使用するので大きな問題はないかと思います。ローカル環境でもハードコードするのを避けたい場合にはそれも可能ですが、今回は割愛します。
</details>


<details>
<summary>「command: --default-authentication-plugin=mysql_native_password」って？</summary>
MySQLの認証方式でネイティブ認証を利用するための設定です。MySQL8.0以降ではプラガブル認証というよりセキュリティ強度の高い認証方式がデフォルトで採用されていますが、ネイティブ認証を使用する設定にしている記事が多く見受けられたので、今回はネイティブ認証にしています。

[MySQL | ネイティブ認証](https://dev.mysql.com/doc/refman/8.0/ja/native-pluggable-authentication.html)
> MySQL には、ネイティブ認証 (プラガブル認証の導入前から使用されていたパスワードハッシュ方式に基づく認証) を実装する mysql_native_password プラグインが含まれています。
</details>

#### Gemfileを編集
```ruby:Gemfile
source 'https://rubygems.org'
gem 'rails', '~> 7.1.2'
```

#### start.shを編集
```shell:start.sh
#!/bin/sh

bundle exec rails s -p ${PORT:-3000} -b 0.0.0.0
```

<details>
<summary>「start.sh」は何をするためのもの？</summary>
Railsアプリを起動する処理（rails server）を行っています。なお、start.shはDockerfile内で実行しています。
</details>



### 4-2. DockerでRailsアプリを生成

Rails newでDockerコンテナにRailsアプリを生成します。

```shell:terminal
# DockerコンテナにRailsアプリを生成する
docker-compose run web rails new . --force --database=mysql --skip-docker
```

<details>
<summary>「--skip-docker」について</summary>
Rails 7.1から、新規のRailsアプリを作成するとDocker関連のファイルが自動生成されるようになり、デフォルトだとそれによって準備したファイルが上書きされてしまいます。それを回避するために、Docker関連のファイルを生成しないように指定しています。

[Railsガイド | Ruby on Rails 7.1 リリースノート](https://railsguides.jp/7_1_release_notes.html)
> 新規Railsアプリケーションでは、デフォルトでDockerがサポートされるようになりました（#46762）。 新しいアプリケーションを生成すると、そのアプリケーションにDocker関連ファイルも含まれます。
</details>

### 4-3. Railsアプリで使用するMySQLの設定とデータベースの作成

Railsアプリのデータベースの設定をdatabase.ymlで行い、データベースを作成します。

#### database.ymlを変更

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

passwordとhostを以下の通りに修正してください。
```yml:config/database.yml (変更箇所)
  password: password
  host: db
```

<details>
<summary>パスワードを記載してしまって問題ないの？（前述の通り）</summary>
このパスワードはローカル環境（development, test）で使用するものであり、本番環境（production）では別のパスワードを使用するので大きな問題はないかと思います。ローカル環境でもハードコードするのを避けたい場合にはそれも可能ですが、今回は割愛します。
</details>

<details>
<summary>「host: db」とは？</summary>
docker-compose.ymlで定義した「db」のデータベースを使用するための設定です。
</details>

#### Dockerコンテナを起動し、データベースを作成
```shell:terminal
# Dockerコンテナを起動する（「Use Ctrl-C to stop」と表示されるまでしばらく待つ）
docker-compose up

# データベースを作成する（docker-compose upを実行したターミナルとは別のターミナルを開いて実行する）
docker-compose exec web rails db:create
```

#### ブラウザからRailsアプリにアクセスできることを確認
```shell:terminal
open http://localhost:3000/
```
ブラウザが起動し、以下のRailsアプリの初期画面が表示されればOKです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623342/b68ca052-9b5b-8661-e7a4-029a282fe83e.png)



## 5. 本番環境（production）向けの設定とDockerによるデプロイ

本番環境としてHerokuを利用するための設定と、Dockerコンテナによるデプロイを行います。

### 5-1. Heroku CLIの導入とHerokuの各種設定
#### Homebrewをインストール（手順は割愛）
HerokuへのデプロイはHeroku CLIというコマンドラインツールを使用して行います。
Heroku CLIをインストールするには、その前提としてHomebrewというパッケージ管理ツールをMacにインストールする必要があります。Homebrewのインストール手順はこちらでは割愛します。

https://brew.sh/

#### Heroku CLIをインストール
Homebrewがインストールされた状態で、以下のコマンドでHeroku CLIをインストールします。
```shell:teminal
​brew tap heroku/brew && brew install heroku
```

https://devcenter.heroku.com/ja/articles/heroku-cli


#### HerokuにログインしてHerokuアプリを作成

```shell:terminal
# Heroku CLIでHerokuにログイン（ブラウザで画面が表示されるので「Log in」をクリックする
heroku login

# 「heroku: Press any key to open up the browser to login or q to exit:」と表示されたらEnterを押す

# コンテナレジストリにログイン
heroku container:login

# Herokuにアプリを作成
heroku create <任意のHerokuアプリ名>
```
※Herokuアプリ名は世界中で一意である必要があります。

https://devcenter.heroku.com/ja/articles/app-names-and-subdomains
> アプリ名はアプリケーションの一意識別子です。

### 5-2. 本番環境向けのMySQLの設定
Herokuアプリでデータベースを利用するために、HerokuのMySQL(JawsDB)のアドオンを追加し、接続情報などを設定します。

#### HerokuアプリにMySQLのアドオンを追加
```shell:terminal
# アプリケーションで利用するデータベース（MySQL）のアドオンを追加
heroku addons:create jawsdb:kitefin
```

<details>
<summary>JawsDBってなに？</summary>
Herokuで利用可能なMySQLのアドオンの1つです。MySQLだけでもさまざまなアドオンが用意されており、アドオンごとに性能や価格などが異なります。JawsDBにもいくつかプランがあり、Kitefin Sharedというプランが現時点では無料で、今回はこちらを使用しています。

[Heroku | JawsDB MySQL](https://elements.heroku.com/addons/jawsdb)
> The database you trust, with the power and reliability you need. Starting at ~$0/hour.
</details>

#### 本番環境向けのデータベースの設定
database.ymlで、以下のように本番環境（production）の設定を行います。


```yml:config/database.yml (変更前)
production:
  <<: *default
  database: rails_app_production
  username: rails_app
  password: <%= ENV["RAILS_APP_DATABASE_PASSWORD"] %>
```

```yml:config/database.yml (変更後)
production:
  <<: *default
  database: <%= ENV['DATABASE_NAME'] %>
  username: <%= ENV['DATABASE_USERNAME'] %>
  password: <%= ENV['DATABASE_PASSWORD'] %>
  host: <%= ENV['DATABASE_HOST'] %>
  url: <%= ENV['DATABASE_URL'] %>
```
※データベースの接続情報をハードコードせずに、環境変数から読み込めむようにしています。

### 5-3. 本番環境向けの環境変数やスクリプトの設定
Herokuアプリのデータベースの接続情報を取得します。
```shell:terminal
# データベースの接続情報を表示
heroku config | grep JAWSDB_URL
```
上記を実行すると、以下の形式でデータベースの接続情報が表示されます。
```shell
# データベースの接続情報の形式
JAWSDB_URL: mysql://<ユーザー名>:<パスワード>@<ホスト名>:3306/<データベース名>
```

この接続情報を参照しながら、環境変数に接続情報を設定します。

```shell:terminal
heroku config:set DATABASE_NAME='<データベース名>'
heroku config:set DATABASE_USERNAME='<ユーザー名>'
heroku config:set DATABASE_PASSWORD='<パスワード>'
heroku config:set DATABASE_HOST='<ホスト名>'
heroku config:set DATABASE_URL='mysql2://<JAWSDB_URLの先頭を「mysql2://」に書き換えて設定>'
```

※元のURLは「mysql://」というスキームの指定になっていますが、その部分を「mysql2://」に書き換えてして設定してください。

<details>
<summary>なんで「mysql2://」にするの？</summary>
config/database.ymlで「adapter: mysql2」を指定しているため、ここでもmysql2を指定する必要があります。これにより、mysqlよりも新しいmysql2を使用してデータベースに接続します。

</details>



#### Heroku上でRailsアプリが本番環境の設定で起動するように環境変数を設定

```shell:terminal
# Railsアプリを本番環境の設定で起動する設定
heroku config:set RAILS_ENV=production
```

#### 本番環境でアセットファイルをコンパイルする処理をstart.shに追加
```shell:start.sh(変更前)
#!/bin/sh

bundle exec rails s -p ${PORT:-3000} -b 0.0.0.0
```
```shell:start.sh(変更後)
#!/bin/sh

if [ "${RAILS_ENV}" = "production" ]
then
    bundle exec rails assets:precompile
fi

bundle exec rails s -p ${PORT:-3000} -b 0.0.0.0
```


<details>
<summary>「bundle exec rails assets:precompile」とは？</summary>
アセットファイル（app/assetsディレクトリ内のファイル）をコンパイルして利用可能にする処理です。
本番環境の場合はデフォルトでアセットファイルがコンパイルされず使用できないので、本番環境でRailsアプリが起動される場合に、事前コンパイルするようこの処理を追加しています。

[Railsガイド | アセットパイプライン](https://railsguides.jp/asset_pipeline.html)
>　app/assetsにあるファイルそのものは、productionで直接配信されることは決してありません。
</details>


### 5-4. HerokuにDockerをデプロイ

```shell:terminal
# ローカル環境のdockerコンテナを停止する
docker compose down

# HerokuにDockerイメージをプッシュし、リリースする。（デプロイ）
heroku container:push web
heroku container:release web

# Herokuのデータベースのマイグレーションを行う
heroku run rails db:migrate

# Herokuアプリにブラウザでアクセスする
heroku open
```

以下の画面が表示されればOKです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623342/65caf436-85fb-6eb3-c72c-6649feafe112.png)


上記はエラー画面なのでちょっと後味が悪いですが、ローカル環境で表示されていたRailsアプリの初期画面はprodction環境では表示されないので、正しくデプロイが行われた状態でこの画面になります。
あとは、実際にコントローラ、ビュー、ルーティングの設定などを行なって、ローカル環境と本番環境の両方でRailsアプリが動作することを確認してください。

## おわりに
最後まで読んでいただきありがとうございました。少しでも参考になれば嬉しいです。

今回は、Advent Calendarというイベントのおかげで技術記事を書くきっかけをいただけました。
自分の理解を深めつつ、誰かの役に立てるかもしれないという点が非常に面白いと感じたので、今後も積極的に書いていきたいと思います。