RailsのToDoアプリチュートリアル（on Docker）

Ruby on Rails始めました。

## 概要

- Ruby on Railsを始めました
- どうやら`sprockets`というのが初学者には曲者らしい。面倒なので、使わないことにした
- `docker-compose`でJSビルド用nodeコンテナ、ファイル配信用nginxを置いて、RailsはAPIの提供だけをすることにした
- 結構大変だった


リポジトリはこちら
[KeitaMoromizato/rails_on_docker](https://github.com/KeitaMoromizato/rails_on_docker)

## 構成

- nginxがすべてのリクエストを受ける。静的ファイルは`www`ボリュームに格納されたファイルを配信、APIへのリクエストはRailsコンテナへ
- nodeコンテナがJavaScriptをビルドして`www`ボリュームにエクスポート
- DBはMySQLを仕様。データは`mysql_data`ボリュームで永続化

![docker.png](https://qiita-image-store.s3.amazonaws.com/0/52054/c5c1cf51-7bf7-7037-a19d-d49b3e673113.png)

## ハマリポイント
### Gemのインストール先
Rubyのgem（nodeでいうnpm）はデフォルトのインストール先が、アプリケーションのディレクトリ内ではなくシステムのディレクトリになっている。これがDockerで動かすときに問題になる。

Dockerコンテナは、起動するたびに等価なものが起動するようにできている。つまり、コンテナ上のシステムディレクトリ（という表現でいいのか）にGemをインストールしたとしても、コンテナを立ち上げ直すとそれは無かったことになる。

ひとつの案としては、`Dockerfile`内で`bundle install`を実行、Gemもまとめてイメージに突っ込む方法がある。世のサンプルを見ても、こうしているケースが多いのだけれど、これだと開発中にGemを新しくインストールしようと思うと`docker-compose build`し直さないといけない。

```:Dockerfile
RUN \
  bundle install
```

解決策としては、Gemをインストールするディレクトリを変更し、Dockerボリュームとして永続化するか、ホストPCのディレクトリにマウントして保存するかという手法が考えられる。

どうやらRails界隈では、アプリケーションディレクトリの`vendor/bundle`にGemのインストール先を変更するのが主流？らしい。という訳で、今回はマウントされたアプリケーションディレクトリの`vendor/bundle`に保存することにした。

Gemのインストール先を変更するには、`bundle config`コマンドを実行する。Dockerfile上にこれを記述することで、コンテナ上で`bundle install`するときのディレクトリが変わる。

```:Dockerfile
RUN \
  bundle config --local path vendor/bundle
```

アプリケーションディレクトリは、`docker-compose`でマウントしている。

```yml:docker-compose.yml
  app:
    build: .
    volumes:
      - .:/usr/src/rails_on_docker
```

ちなみに、`bundle install`をコンテナ上で実行するコマンドはこちら。`app`の部分は、`docker-compose`に記載されているコンテナ名。

```
$ sudo docker-compose run --rm app bundle install
```

### CSRFの設定

RailsのAPI化は以下の手順で行った。

1. レスポンスをJSON化するために`config/routes.rb`に`defaults: { format: 'json' }`を追加
2. CSRFトークンを無視する設定
3. ページネーションのために`kaminari`を導入

RailsはCSRF対策のため、CSRFトークンをチェックして不正なPOSTリクエストを弾いている。これは普通にRailsを書いていれば、Formにtokenが自動的に埋め込まれているため、特に気にすることはない。

ただし、API化する場合はリクエストヘッダに自分でtokenを埋めなければならない。もしsprocketsを使っているのなら、JSからCSRFトークンを取れるらしいが、今回はRailsとJSを完全に分離しているので、それはできない。

ということで調べた所、下記のようにCSRFトークンのチェックをスキップする処理を入れると良いらしい。

```rb:app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  skip_before_filter :verify_authenticity_token
end
```

## 手順
### 1. Railsアプリを作る

[RailsアプリをDockerで開発するための手順](http://qiita.com/togana/items/30b22fc39fe6f7a188ec)を参考に、`rails new`までを実施。

### 2. Dockerfileの準備

Railsアプリを動かすDockerイメージを作成。まだこなれていない。。。

```Dockerfile
FROM ruby:2.3

ENV APP_ROOT /usr/src/rails_on_docker

WORKDIR $APP_ROOT

RUN apt-get update && \
    apt-get install -y nodejs mysql-client postgresql-client sqlite3 --no-install-recommends && \
    rm -rf /var/lib/apt/lists/*

COPY Gemfile $APP_ROOT
COPY Gemfile.lock $APP_ROOT

RUN \
  echo 'gem: --no-document' >> ~/.gemrc && \
  cp ~/.gemrc /etc/gemrc && \
  chmod uog+r /etc/gemrc && \
  bundle config --global build.nokogiri --use-system-libraries && \
  bundle config --global jobs 4 && \
  bundle config --local path vendor/bundle && \
  bundle install && \
  rm -rf ~/.gem

COPY . $APP_ROOT

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

### 3. docker-composeのセットアップ

```yml:docker-compose.yml
version: '2'
services:
  app:
    build: .
    command: rake unicorn:start
    environment:
      RAILS_ENV: development
      DATABASE_URL: mysql2://root:pass@mysql:3306
      MYSQL_ROOT_PASSWORD: 'pass'
    ports:
      - '3000:3000'
    volumes:
      - .:/usr/src/rails_on_docker
    links:
      - mysql
  mysql:
    image: mysql:5.7.10
    environment:
      MYSQL_ROOT_PASSWORD: 'pass'
    ports:
      - '3306:3306'
    volumes:
      - mysql-data:/var/lib/mysql
  node:
    image: node:6.9
    command: npm run watch
    ports:
      - '1234:1234'
    working_dir: /client
    volumes:
      - www:/client/out
      - ./client:/client
  nginx:
    image: nginx
    ports:
      - '80:8080'
    volumes:
      - ./nginx:/etc/nginx/conf.d:ro
      - www:/www/app:ro
    links:
      - 'app'
volumes:
  mysql-data:
    driver: local
  www:
    driver: local
```

### 4. railsアプリの初期設定

今回は、ModelひとつのみのToDoアプリを構築する。`Task`モデルをRailsのscaffoldを使って作成する。

```
$ sudo docker-compose run --rm app rails generate scaffold Task content:text
```

ついでにgemのインストールと、DBのmigrateも実行。

```
$ sudo docker-compose run --rm app bundle install
$ sudo docker-compose run --rm app rake db:migrate
```

### 5. nginxコンテナの設定

`nginx/default.conf`にあるファイルを参照。ルートとリソースへのアクセスは、静的ファイルをそのまま配信し、`/api`へのアクセスはRailsアプリへフォワードする（`/api/task`がRailsアプリ上からは`/task`に見える）。

```nginx/default.conf
server {
  listen 8080;
  server_name localhost;
  root /www/app;

  location /api/ {
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_pass http://app:3000/;
  }

  location = / {
    index index.html;
  }

  location ~ /r/(.+\.(js|css|jpg|png))$ {
    expires -1;
    alias /www/app/$1;
  }
}
```

### 6. nodeコンテナの設定

長いので`client`ディレクトリを参照。

https://github.com/KeitaMoromizato/rails_on_docker/tree/master/client

Gemと同様に、npmのインストールはコンテナ上で。

```
$ sudo docker-compose run --rm node npm install
```

### 7. RailsアプリのAPI化
#### レスポンスのJSON化

```rb:config/routes.rb
Rails.application.routes.draw do
  resources :tasks, defaults: { format: 'json' }
```
#### CSRF対策対策

```rb:app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  skip_before_filter :verify_authenticity_token
end
```
### 7. Railsアプリのページネーション

`kaminari`を使用するので、`Gemfile`に追記。

```rb:Gemfile
gem 'kaminari'
```

`bundle install`を実行してgemをインストール。

```
$ sudo docker-compose run --rm app bundle install
```

コントローラーにページネーションの設定を追記。

```rb:app/controllers/task_controller.rb
  def index
    @tasks = Task.page(params[:page])
    render json: @tasks.to_json(:except => [:url])
  end
```

## まとめ

Gemのインストールのところで特に詰まって、普通にチュートリアルをするより時間がかかった。