開発しやすいRails on Docker環境の作り方

最近、Rails界隈でDocker使い始めました、という話を聞く機会が増えてきたので、自分が開発環境整備用に構築したDockerの設定をまとめておく。

ちなみに、production運用については以前書いたので適当に探してくださいw

結論から書いておくと、volumeをちゃんと活用すればいい、ってだけの話です。

まず、本番用と開発用のDockerfileは分けた方が良い。一つでやろうとするとどうにも無理がでるので。
自分はDockerfileとDockerfile-devというものを用意している。

docker-composeはほぼ必須です。少なくともrailsプロセスとDBだけでも二つは必要だし、Dockerfileを分けてると事故るので。

Dockerfileはこんな感じ。

```dockerfile
FROM mybase:ruby-2.3.1-debian

RUN echo "deb http://http.debian.net/debian jessie-backports main" >> /etc/apt/sources.list \
  && apt-get update \
  && apt-get install -y --no-install-recommends qt5-default libqt5webkit5-dev gstreamer1.0-plugins-base gstreamer1.0-tools gstreamer1.0-x xvfb \
  && rm -rf /var/lib/apt/lists/*
ENV DISPLAY :99

WORKDIR /app

ENV DOCKER 1

RUN bundle config build.nokogiri --use-system-libraries

ENTRYPOINT [ \
  "prehook", "ruby -v", "--", \
  "prehook", "bundle install -j3 --quiet --path vendor/bundle", "--", \
  "prehook", "npm install --no-optional", "--", \
  "prehook", "bower install --allow-root", "--", \
  "prehook", "sh docker/xvfb.sh", "--", \
  "prehook", "ruby docker/setup.rb", "--"]

# vim:ft=dockerfile
```

ENTRYPOINTには、entrykitを利用しているが、ここは各自で色々と調整。
重要なのはコンテナを起動したら絶対にやっておかなければいけないことは、強制的に実行されるようにしておくこと.
そして、Railsのプロセスにちゃんとシグナルが届くようにしておくこと。
単純にシェル噛ますとかだとシグナルがシェルにいってしまって駄目なので注意。
やることは、大体bundlerやnpmの実行ですね。capybara-webkitを使ってるのでxvfbの起動とかもこのタイミングでやっている。他は、APIキー等をS3から取ってきたりとか。
まあ、rubyが入っててテストが実行できるだけの環境があれば、余りやることはない。
重要なのは、docker-compose.ymlの方。

docker-compose.ymlは以下のような感じ。

```yaml
version: "2"
services:
  datastore:
    image: busybox
    volumes:
      - mysql-data:/var/lib/mysql
      - redis-data:/data
      - bundle_install:/app/vendor/bundle
      - bundle:/app/.bundle
      - node_modules:/app/node_modules

  mysql:
    image: my-mysql
    environment:
      MYSQL_ROOT_PASSWORD: password
    networks:
      - default
    ports:
      - '3306:3306'
    volumes_from:
      - datastore

  redis:
    image: redis:alpine
    networks:
      - default
    ports:
      - '6379:6379'
    volumes_from:
      - datastore

  app:
    build:
      context: .
      dockerfile: Dockerfile-dev
    ports:
      - '3000:3000'
    environment:
      MYSQL_USERNAME: root
      MYSQL_PASSWORD: password
      MYSQL_HOST: mysql
      REDIS_URL: "redis://redis:6379"
      AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
      AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
    depends_on:
      - mysql
      - redis
    networks:
      - default
    volumes:
      - .:/app
    volumes_from:
      - datastore
    command: ["bundle", "exec", "rails", "s", "-b", "0.0.0.0"]

volumes:
  mysql-data:
    driver: local
  redis-data:
    driver: local
  bundle_install:
    driver: local
  bundle:
    driver: local
  node_modules:
    driver: local
```

大事なのは、プロジェクトディレクトリをvolumeとしてマウントすることと、bundleやnpmでインストールするものに対して別途volumeを定義することです。
後は、networksをちゃんと定義して、コンテナの名前をホスト名として利用できるようにすることですが、これはほとんどの場合docker-composeが勝手に処理してくれます。
サブネットを分けたりしてネットワーク構成をできるだけ本番に近付けたい、といった場合等はここで調整することも可能です。

こうしておくと、アプリケーションコードへの変更は直接Dockerコンテナ内に反映されるので、大体の変更は再起動も不要で普通にRailsが再読み込みしてくれます。
bundleでインストールする先は、dockerが別途docker上のvolumeに永続化されるので、手元がMacでdocker上はLinuxという状況でも、モジュールが混ざらず済むし、コンテナを消しても同じvolumeが再利用できてデータが残るんでbundle installがすぐに終わります。
npmも同様ですね。

ついでに、DBやredisのデータ格納領域もちゃんと定義しておくと、DBの結果もちゃんと残せます。
要は、冒頭に書いたようにvolumeを活用しようって話ですね。

サーバープロセスを起動しつつ、`docker-compose exec`を使えばコンテナ内にシェルで入って調査とかもできるので、pryとかを使いたい場合はそれで可能です。byebugを使いたい場合は一回bashで起動してからコンテナ内で手動でサーバー上げた方が良いかもしれません。

ちなみに、Mac上で開発している場合、カレントディレクトリをdockerに直でマウントすると、パフォーマンス上かなり辛いという問題があります。
基本的にMacでDockerを動かす場合VMを経由することになるため、VMとMac間のデータのやり取りにオーバーヘッドがあります。
docker-machineを使っても、Docker for Macを使ってもかなり辛い。
現状dinghyが一番マシっぽいです。3つとも試しましたが、結局自分はdinghy使ってます。
dinghyはNFSサーバーをMac上で動かすことでディレクトリをVM側にエクスポートします。これは割とまともな速度で動きます。
正直、Docker for MacがストレージマウントしたらCPU食いまくるのは納得いかないし、早く何とかしてくれー！、という感じです。
なんせ、これができないと変更の度にイメージビルドが必要になって最悪なので……。