Docker Compose と Simpacker で、モダンな Rails 開発環境を構築する

# 想定読者
* Rails のフロントエンド開発に webpack を利用している（利用しようとしている）
    * Rails/webpack 自体の設定については、本記事では必要最低限しか記載しません
    * ※ webpacker gem 経由で webpack を利用されている場合
        * →本記事では webpacker は利用しません。webpack 自体の設定を直接書くことになります。
* Rails 実行用のコンテナと webpack 実行用のコンテナを分離したい＆それぞれのコンテナサイズを小さくしたい
    * コンテナの再ビルド時間を減らしたい
    * Node.js に依存する処理が Rails コード内に存在しないことを簡単に検知したい
    * ただの趣味
    * etc.

# 環境
* Ruby >= 2.5.1 (for Rails 6)
    * with Bundler
* Node.js >= 12.x
    * with Yarn
* Docker
    * with Docker Compose

※ 一部手順を簡略化するため、Ruby および Node.js がローカルにもインストールされていることを前提としています
`docker run ruby -v .:/app -w /app` などを使えばローカルインストールせずとも構築できるとは思いますが未確認です

# 手順
## 必要なパッケージのインストール
### Rails new の実行
```sh
cd path/to/workspace
gem install rails
rails new <my-new-rails-project> --database=postgresql --skip-sprockets --skip-javascript --skip-turbolinks --skip-bundle --skip-webpack-install
```
* `<my-new-rails-project>`, `--database=` あたりの指定は適宜読み替えてください
* css(sass) の管理も webpack に任せる前提となっています。css(sass) の管理を Rails で行いたい場合は `--skip-sprockets` オプションは指定しないでください

### Simpacker gem のインストール
```sh
cd path/to/workspace/<my-new-rails-project>
echo "gem 'simpacker'" >> Gemfile
bundle install
bundle exec rails simpacker:install
```

### webpack-dev-server のインストール
```sh
cd path/to/workspace/<my-new-rails-project>
yarn add -D webpack-dev-server
```

### Dockerfile の作成
#### for Rails
```docker
# <my-new-rails-project>/build/Dockerfile.rails
FROM ruby:2.7.0 as rails-dev

RUN apt-get install -y locales \
        && echo 'ja_JP.UTF-8 UTF-8' >> /etc/locale.gen \
        && locale-gen \
        && update-locale LANG=ja_JP.UTF-8
ENV LANG=ja_JP.UTF-8

RUN mkdir /app
WORKDIR /app

ADD Gemfile /app/Gemfile
ADD Gemfile.lock /app/Gemfile.lock
RUN bundle install

CMD ["bundle" "exec" "rails" "server"]
```

#### for webpack
```docker
# <my-new-rails-project>/build/Dockerfile.rails
FROM node:12 as webpack

RUN mkdir /app
WORKDIR /app

ADD package.json /app/package.json
ADD yarn.lock /app/yarn.lock
RUN yarn install

CMD ["yarn" "run" "webpack-dev-server" "--config" "webpack.config.js"]
```

### docker-compose.yml
```yaml
# <my-new-rails-project>/docker-compose.yml
version: '3.7'

services:
  rails:
    build:
      context: .
      dockerfile: build/Dockerfile.rails
      target: rails-dev
    tty: true
    stdin_open: true
    ports:
      - "3000:3000"
    volumes:
      - .:/app
      - bundle:/usr/local/bundle
    depends_on:
      - db
      - webpack
  webpack:
    build:
      context: .
      dockerfile: build/Dockerfile.webpack
      target: webpack
    ports:
      - "3035:3035"
    volumes:
      - .:/app
      - node_modules:/app/node_modules
  db:
    image: postgres:12
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    volumes:
      - pg_data:/var/lib/postgresql/data
volumes:
  bundle: {}
  node_modules: {}
  pg_data: {}
```

## Rails/webpack それぞれの設定を変更
### Rails
#### config/puma.rb
docker コンテナ外からアクセスするために、listen address を `127.0.0.1` から `0.0.0.0` に変更します

```diff
 # Specifies the `port` that Puma will listen on to receive requests; default is 3000.
 #
-port        ENV.fetch('PORT') { 3000 }
+port        ENV.fetch('PORT') { 3000 }, '0.0.0.0'

 # Specifies the `environment` that Puma will run in.
 #
```

#### config/database.yml
`db` コンテナに接続する例です（適宜読み替えてください）

```diff
   # For details on connection pooling, see Rails configuration guide
   # https://guides.rubyonrails.org/configuring.html#database-pooling
   pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
+  username: postgres
+  password: postgres
+  host: db

 development:
   <<: *default
```

### webpack.config.js
See also: https://github.com/hokaccha/simpacker/blob/master/example/webpack-dev-server/README.md

```diff
   output: {
     path: path.resolve(__dirname, "public/packs"),
-    publicPath: "/packs/",
+    publicPath: isProd ? "/packs/" : "//localhost:3035/packs/",
     filename: isProd ? "[name]-[hash].js" : "[name].js"
   },

   ...

   },
-  plugins: [new WebpackAssetsManifest({ publicPath: true })]
+  plugins: [new WebpackAssetsManifest({ publicPath: true, writeToDisk: true })]
+  devServer: {
+    host: "0.0.0.0",
+    port: 3035,
+    hot: true,
+    headers: {
+      "Access-Control-Allow-Origin": "*"
+    }
+  }
 }
```

## 動作確認
### Rails controller/view の作成
#### config/routes.rb
```ruby
Rails.application.routes.draw do
  root 'home#index'
end
```

#### app/controllers/home_controller.rb
```ruby
class HomeController < ApplicationController
  def index; end
end
```

#### app/views/layouts/application.html.slim
```slim
doctype html
html[lang="ja"]
  head
    meta[charset="UTF-8"]
    title MyNewRailsProject
    = csrf_meta_tags
    = csp_meta_tag
    = javascript_packs_tag 'application'

  body
    = yield
```

#### app/views/home/index.html.slim
```slim
h1 home#index
```

### Rails server の起動
```sh
docker-compose build
docker-compose up -d
open http://localhost:3000 # for macOS
```

# 参考
* http://docs.docker.jp/index.html
* https://techlife.cookpad.com/entry/2019/07/08/100000
* https://qiita.com/park-jh/items/65a278f206773179e922