Rails6プロジェクト(API)をDockerで開発(少しイメージの軽量化)

本記事は[東京学芸大学 櫨山研究室 Advent Calendar 2020](https://qiita.com/advent-calendar/2020/hazelab)の15日目の記事になります．

# はじめに
本記事はRails6が出たのでその環境をDocker化し，また少しイメージを軽くする方法です(RDBはMysql8.0を想定)．
起動を確認するところまでを記事にしています．
railsのプロジェクトはローカルで作成したもの使用しています（私はローカルで以下のように作成しています）．

```console:console
$ rails new api --api -d mysql
```

フロントとバックエンドを分けて開発を行いたかったので，Rails6プロジェクトをapiモードで作成しています．
また本記事ではフロントエンド側の記述はしていません．
普段はRailsを使用していないので，メモとして残します🙇‍♂️

# 開発環境
- MacOs:Mac mini (2018) macOS Big Sur ver.11.1
- Docker：20.10.0
- docker-compose:1.27.4
- ruby:2.6.3(rbenv
で管理： [参考記事](https://qiita.com/Kodak_tmo/items/73147ed4f0eec54d6e94))
- Rails 6
- Mysql8.0

# 1. 準備
今回は既存のRails6プロジェクト(apiモード)をDocker化していきます．
## 1.1 ディレクトリ構成
```
├── Makefile
├── README.md
├── api
│   ├── Gemfile
│   ├── Gemfile.lock
│   ├── README.md
│   ├── Rakefile
│   ├── app
│   ├── bin
│   ├── config
│   ├── config.ru
│   ├── db
│   ├── lib
│   ├── log
│   ├── public
│   ├── storage
│   ├── test
│   ├── tmp
│   └── vendor
├── client
│   ├── next-env.d.ts
│   ├── node_modules
│   ├── package.json
│   ├── src
│   ├── tsconfig.json
│   ├── yarn-error.log
│   └── yarn.lock
└── docker
   ├── api
   ├── api.env
   ├── client
   ├── docker-compose.yml
   ├── mysql
   ├── mysql.env
   └── postgreSQL
```
今回はdocker-compose.ymlやDockerfileが階層的になっているため，`make`で実行していきます．

## 1.2 ファイルの準備

#### Makefile
```make:Makefile
.PHONY: build
build:
	docker-compose -f docker/docker-compose.yml build

.PHONY: start
start:
	docker-compose -f docker/docker-compose.yml up --remove-orphans

.PHONY: start.background
start.background:
	docker-compose -f docker/docker-compose.yml up -d --remove-orphans

.PHONY: stop
stop:
	docker-compose -f docker/docker-compose.yml down
```
- `build`でパッケージの取得
- `start`でアプリケーションを実行
- `start.background`でバックグラウンドでアプリケーションを実行
- `stop`でアプリケーションを停止

#### docker-compose.yml

```yml:docker/docker-compose.yml
version: "3"
services:
  api:
    container_name: api
    build:
      context: ../.
      dockerfile: ./docker/api/Dockerfile
    tty: true
    working_dir: /api
    volumes:
      - ../api:/api
    env_file: # dockerディレクトリ直下にapi.envファイルを作成し環境変数を設定する．
      - api.env
    ports:
      - "8000:3000" # フロントエンド側が3000番開放になるので変更
    depends_on:
      - db
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    networks:
      - sample-rails-docker-network

  db:
    container_name: db
    build:
      context: ../.
      dockerfile: ./docker/mysql/Dockerfile
    volumes:
      - ./mysql/db:/docker-entrypoint-initdb.d
    env_file: # dockerディレクトリ直下にmysql.envファイルを作成し環境変数を設定する．
      - mysql.env
    networks:
      - sample-rails-docker-network

networks: # 名前解決できるようにネットワークの設定
  sample-rails-docker-network:
    driver: bridge
```

### Dockerfile：Mysql関連

```Docker:docker/mysql/Dockerfile
FROM mysql:8.0 # mysq;8.0を使用する

EXPOSE 3306
ADD docker/mysql/my.cnf /etc/mysql/conf.d/my.cnf

CMD ["mysqld"]
```
3306番ポートを開放し，設定ファイルを読み込みます.

```docker/mysql/my.cnf
[mysqld]
character-set-server=utf8
default_authentication_plugin=mysql_native_password

[mysql]
default-character-set=utf8

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

mysqlのvolumeは`docker/mysql/db`に設定しているので，必要な場合はそちらに配置してください．

### Dockerfile：Rails 6関連

```Docker:docker/api/Dockerfile
FROM ruby:2.6.3-alpine as builder # ruby:2.6.3-alpineを使用する，bundle installするためにbuilderとして使用 
RUN apk update && \
  apk upgrade && \
  apk --no-cache add mysql-client mysql-dev tzdata build-base && \
  gem install bundler

WORKDIR /tmp
COPY api/Gemfile Gemfile
COPY api/Gemfile.lock Gemfile.lock
RUN bundle install

FROM ruby:2.6.3-alpine
ENV LANG ja_JP.UTF-8 
RUN apk --update add mysql-client mysql-dev tzdata nodejs && \
  gem install bundler

ENV APP_HOME /api
RUN mkdir -p $APP_HOME
WORKDIR $APP_HOME
COPY --from=builder /usr/local/bundle /usr/local/bundle
COPY api $APP_HOME
```

今回使用するimageは`ruby:2.6.3-alpine`ですが，それぞれの環境で変更しても大丈夫です．
マルチステージングにし，`bundle install`にだけ必要なもの(build-base)を省くようにする．(`apk del `で消しても問題ない．)
今回はmysqlが動く最低限の環境になっています．

### Rails関連
#### Gemfile
```rb:api/Gemfile
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby '2.6.3'

# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 6.0.3', '>= 6.0.3.2'

# database
gem 'mysql2'

# Use Puma as the app server
gem 'puma', '~> 4.1'

# Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', '>= 1.4.2', require: false

# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible
gem 'rack-cors'

group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
end

group :development do
  gem 'listen', '~> 3.2'
  # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
  gem 'spring'
  gem 'spring-watcher-listen', '~> 2.0.0'
end

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
```
Gemfileは，`mysql2`，`rack-cors`を設定しています．
`rack-cors`はCORSの設定です．

こちらもそれぞれの開発環境に合わせて変更してください(**変更時にはDockerfileでapk add で加えるパッケージもも変更する必要があるので気をつけてください**).

#### cors.rb
```rb:api/config/initializers/cors.rb
# Be sure to restart your server when you modify this file.

# Avoid CORS issues when API is called from the frontend app.
# Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests.

# Read more: https://github.com/cyu/rack-cors

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins '*'
    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end
```
CORSの設定は全てを許可しているので，それぞれで変更しましょう．

#### ルーティング(routes.rb)
```rb:api/config/routes.rb
Rails.application.routes.draw do
  namespace 'api' do
    namespace 'v1' do
      resources :hoges
    end
  end
end
```
APIなのでルーティングをVersion管理しやすくしておきます．

#### コントローラー(hoges_controller.rb)
```rb:api/app/controllers/api/v1/hoges_controller.rb
module Api
  module V1
    class HogesController < ApplicationController
      def index
        render json: { status: 'SUCCESS', message: 'Hello Rails!'}
      end
    end
  end
end
```

# 2. ビルドとアプリケーションの実行

## 2.1 rails new

今回は既存のプロジェクトを使用したので，省略します．([参考記事](https://qiita.com/k-penguin-sato/items/adba7a1a1ecc3582a9c9))
## 2.2 rake db:create
今回はDBの接続確認はしないので省略します．
参考までにapi直下で，以下のコマンドで環境を作成できます．

```console:console
$ rails g model post title:string text:text
$ rails g controller posts
$ rails db:create
$ rake db:migrate
```

## 2-3. 依存関係の解決&imageの取得

```console:console
$ make build
```

`apk add [パッケージ]`と`bundle install`をしますので，少し時間がかかります．

## 2-4. アプリケーションの実行

```console:console
$ make start
```
Railsとmysqlが起動します．

#### 起動の確認

[起動確認](http://localhost:8000)

![スクリーンショット 2020-12-15 21.09.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273450/6a1d97f4-9b89-acac-5f97-2f0786e67c99.png)

いつもの画面が出てくれば問題なく起動できています．

#### api化の確認

```console:console
% curl http://localhost:8000/api/v1/hoges
{"status":"SUCCESS","message":"Hello Rails!"}%
```
問題なくできています！！

## 2.5 他のイメージと比較

サイズはプロジェクト事態が存在しての大きさになります．

|イメージ|サイズ|
|:--:|:--:|
|ruby:2.6.3|1.1GB|
|ruby:2.6.3-alpine|387MB|
|ruby:2.6.3-alpine(ビルドベース削除)|264MB|

`bundle install`時に必要な`build-base`を削除するだけでかなり削減されています！
# おわりに
Rails6が出たので，そのAPIプロジェクトのDocker化，微量ながら軽量化をしました．
特に元イメージとalpineの差はとても大きいので，イメージサイズを減らすことができてよかったです．
これで好きなフロントエンドを使用してRailsのAPIを使用します．