【Rails】Dockerで Vue+ Rails APIの開発環境を構築する【Mac】

# はじめに
[前回](https://qiita.com/generonishimura/items/88742085294bd0b234a6)Vite+Dockerでフロントエンドの開発環境を作成しました。
[【Vue3】Docker + Viteで Vue3 + TypeScriptの爆速開発環境を構築する【Mac】](https://qiita.com/generonishimura/items/88742085294bd0b234a6)
今回はAPIサーバーをDocker環境に載せていきます。

## 本記事を読む前に
フロントエンドは[前回](https://qiita.com/generonishimura/items/88742085294bd0b234a6)作成した環境を使用します。
`Rails + MySQL`のみの環境を作る際にも利用できるかと思います。

本記事では、各ツールやライブラリの説明は行いません。
あくまで環境構築を目的とした記事となっております。
`Dockerfile`や`docker-compose.yml`の記法・使用するコマンドは事前に学習しておくと、スムーズに進めることが出来ると思います。

# 目次
1. [事前準備/条件](#事前準備/条件)
1. [構築手順](#構築手順)
1. [まとめ](#まとめ)

# 事前準備/条件
 - Macを使用していること
 - Dockerを導入していること

Dockerをインストールしていない方は[こちら](https://docs.docker.com/desktop/install/mac-install/)からインストールしてください。
Windowsだと一部のコマンドが動作しない場合がありますので、あくまで参考程度にご覧いただければと思います。

# 構築手順
### 各種ファイルの作成
任意の作業ディレクトリを作成し、`Dockerfile`を作成します。
※前回作成したフロントエンドの環境は`ｆrontend`に移動します(cpコマンドかmvコマンドを使用してください)。

```Bash:zsh
$ mkdir docker_vue3
$ cd docker_vue3
$ mkdir frontend
$ mkdir backend
$ mkdir db
$ cd backend
$ touch Dockerfile
$ touch Gemfile
$ touch Gemfile.lock
$ touch entrypoint.sh
```

 ``` Dockerfile:backend/dockerfile
# ベースとなるイメージ
FROM ruby:3.1.3 as base
WORKDIR /app
ENV NODE_MAJOR_VERSION 16

# 各種ライブラリをインストール
FROM base AS builder
RUN curl -sL https://deb.nodesource.com/setup_$NODE_MAJOR_VERSION.x | bash - && \
    apt-get update -qq && \
    apt-get install -y --no-install-recommends \
    vim locales build-essential curl libpq-dev libmariadb-dev nodejs yarn nginx sudo default-mysql-client openssl

# bundle install
FROM builder as bundle
COPY Gemfile /app/Gemfile
COPY Gemfile.lock /app/Gemfile.lock
RUN bundle install

# 最終的に使用するイメージ
FROM bundle as main
COPY . .

# railsの起動設定
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000

CMD ["rails", "server", "-b", "0.0.0.0"]
 ```

今回は[マルチステージビルド](https://matsuand.github.io/docs.docker.jp.onthefly/develop/develop-images/multistage-build/)を使用しdockerfileを作成します。
マルチステージビルドを使用し、レイヤーを追加していくことで、イメージサイズを小さくすることが出来ます。

### Gemfileの作成
```Gemfile:backend/Gemfile
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby '3.1.3'
gem 'rails', '~> 6.1.7'
```
`dockerfile`で指定しているrubyバージョン`3.1.3`と、`rails`のバージョン指定をします。
今回は`6.1.7`を使用します。

### Gemfile.lockの作成
`M1 Mac`の場合、Platformの指定をしなければいけません。

```Gemfile.lock:backend/Gemfile.lock
PLATFORMS
  aarch64-linux
```

### entrypoint.shの作成
[こちら](https://matsuand.github.io/docs.docker.jp.onthefly/samples/rails/)を参考に`entrypoint.sh`の作成を行います。

```Bash:backend/entrypoint.sh
#!/bin/bash
set -e

# Rails に対応したファイル server.pid が存在しているかもしれないので削除する。
rm -f /myapp/tmp/pids/server.pid

# コンテナーのプロセスを実行する。（Dockerfile 内の CMD に設定されているもの。）
exec "$@"
```

### docker-compose.ymlの作成
続いてdocker-compose.ymlファイルを作成します。

#### docker-compose.ymlの前準備
前回作成した`frontend`の環境に関して以下対応します。
1. フォルダ名変更→ `ｆrontend`
1. `Dockerfile`の配置先を`frontend/Dockerfile`に変更
1. `docker-compose.yml`の`port`を`8000`に変更
1. `vite.config.ts`の`port`も`8000`に変更します。

```typescript:frontend/vite.config.ts
~~~省略~~~
  server: {
    host: true,
    port: 8000,
  },
```


#### docker-compose.ymlの作成

```docker-compose.yml:docker-compose.yml
version: '3.9'
services:
  frontend:
    build:
      # frontend配下に変更
      context: ./frontend
      dockerfile: Dockerfile
    working_dir: /app
    volumes:
      - ./frontend:/app
    ports:
      - '8000:8000'
    networks:
      vite_rails_net:
        ipv4_address: '172.28.0.3'

  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - './backend:/app'
    environment:
      TZ: UTC
      # DBに設定したIPを指定
      DB_HOST: 172.28.0.5
      MYSQL_ROOT_PASSWORD: $MYSQL_ROOT_PASSWORD
    ports:
      - '3000:3000'
    networks:
      vite_rails_net:
        ipv4_address: '172.28.0.4'
    # backendを起動する際はDBも起動する
    depends_on:
      - db

  db:
    platform: linux/x86_64
    command: --default-authentication-plugin=mysql_native_password
    image: mysql:8.0.28
    volumes:
      - './db/data:/var/lib/mysql'
    environment:
      MYSQL_ROOT_PASSWORD: $MYSQL_ROOT_PASSWORD
    ports:
      - '3306:3306'
    networks:
      vite_rails_net:
        ipv4_address: '172.28.0.5'

# ネットワーク作成
networks:
  vite_rails_net:
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 172.28.0.0/24
```

### .envの作成
`docker-compose.yml`内で`$MYSQL_ROOT_PASSWORD`を設定していますが、こちらは.envに記載することで、環境変数を使用できます。
`docker-compose.yml`と同階層に配置します。
```Bash:zsh
$ touch .env
```
MySQLの環境変数に関しては[こちら](https://hub.docker.com/_/mysql/)を参照してください。

```env:.env
MYSQL_ROOT_PASSWORD=hogehoge
```


### dockerのイメージ作成
`Dockerfile`と`docker-compose.yml`の作成が出来たらdockerのイメージをビルドします。

``` Bash:zsh
$ docker compose build
```

### railsコマンドでプロジェクトの作成
`bundle install`を`docker compose build`で完了しているので、`rails`コマンドが使用できるようになりました。
 `rails`を使用してプロジェクトを作成します。

```Bash:zsh
$ docker compose run backend rails new . --api --force --database=mysql
[+] Running 13/13
 ⠿ db Pulled                                                                              15.1s
   ⠿ 4be315f6562f Pull complete                                                            3.0s
   ⠿ 96e2eb237a1b Pull complete                                                            3.1s
~~~省略~~~
[+] Running 1/1
 ⠿ Container database  Started                                                             0.3s
       exist  
      create  README.md
      create  Rakefile
      create  .ruby-version
      create  config.ru
      create  .gitignore
      create  .gitattributes
       force  Gemfile
~~~省略~~~
      create  app
      create  app/assets/config/manifest.js
      create  app/assets/stylesheets/application.css
      create  app/channels/application_cable/channel.rb
      create  app/channels/application_cable/connection.rb
      create  app/controllers/application_controller.rb
      create  app/helpers/application_helper.rb
      create  app/javascript/channels/consumer.js
      create  app/javascript/channels/index.js
      create  app/javascript/packs/application.js
      create  app/jobs/application_job.rb
      create  app/mailers/application_mailer.rb
      create  app/models/application_record.rb
      create  app/views/layouts/application.html.erb
      create  app/views/layouts/mailer.html.erb
      create  app/views/layouts/mailer.text.erb
      create  app/assets/images
      create  app/assets/images/.keep
      create  app/controllers/concerns/.keep
      create  app/models/concerns/.keep
      create  bin
      create  bin/rails
~~~省略~~~
      remove  config/initializers/new_framework_defaults_6_1.rb
         run  bundle install
Fetching gem metadata from https://rubygems.org/...........
Resolving dependencies...
Using rake 13.0.6
Using zeitwerk 2.6.7
Using builder 3.2.4
~~~省略~~~
         run  bundle binstubs bundler
```

`backend`は`docker-compose.yml`で指定したService名です。
各種ファイルが生成されたのでデータベースを作成していきます。


### データベース接続設定

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

development:
  <<: *default
  database: app_development
```

### データベース作成

```Bash:zsh
$ docker compose build backend
$ docker compose run backend rails db:create --trace
```

`--trace`コマンドを付けておくと接続が出来なかった際のデバッグがしやすいです。

# 実行確認
`docker compose up`でサーバー起動をします。

```Bash:zsh
$ docker compose up
```

`0.0.0.0:3000`か`localhost:3000`にアクセスします。


![スクリーンショット 2023-02-28 13.37.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730384/da243a9f-1008-23e1-2eb3-1c04ea76aad7.png)

無事接続できました。

# フロントエンドからAPIを叩く

まずは`axios`をフロントエンドに導入します。

```Bash:zsh
$ docker compose run frontend yarn add axios
```

続いて`/api`でバックエンドに`proxy`するように設定します。

```typescript:frontend/vite.config.ts
~~~省略~~~
  server: {
    host: true,
    port: 8000,
    proxy: {
      '^/api': {
        // docker-compose.ymlに記載しているbackendのIPを指定
        target: 'http://172.28.0.4:3000',
        changeOrigin: true,
      },
    },
  },
```

最後に`App.vue`がマウントされた際に`API`を叩くように`App.vue`を編集していきます。
ついでに、APIが返却値を画面に表示するようにします。

```vue:frontend/src/App.vue
<script setup lang="ts">
~~~省略~~~
import { onMounted, ref } from 'vue'
import axios from 'axios'

const message = ref<string>('')
onMounted(async () => {
  const res = await axios.get('api')
  message.value = res.data.message
})
</script>
<template>
~~~省略~~~
  <div>{{ message }}</div>
~~~省略~~~
</template>
```

### APIの作成

`controller`を作成します。

```Bash:zsh
$ touch backend/app/controllers/api_controller.rb
```

```ruby:backend/app/controllers/api_controller.rb
class ApiController < ApplicationController
  def index
    render json: {message: 'Hello World'}, status: 200
  end
end
```

続いてルーティングの設定を行います。

``` backend/config/routes.rb
Rails.application.routes.draw do
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
  resources :api do
    get '/', to: 'api#index'
  end
end
```

`localhost:8000` にアクセスします。

![スクリーンショット 2023-02-28 14.55.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730384/d7b4fe5b-b11a-9430-443a-e53eeb111c44.png)

`axios`からAPIが叩かれ`Hello World`が返却されていることが確認できました。

以上が環境構築となります。

# まとめ
今回はVue + Rails(MySQL)環境を作成していきました。
開発中に何度もビルドしますが、マルチステージビルドを使用することで、ビルドの時間を短くすることができます。