注目の記事

DockerでRailsの開発環境を構築する

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

モバイルアプリサービス部の五十嵐です。

先日、あるRailsアプリケーションの開発環境を同僚のマシンに作成しようとしたところ、gemのインストールに1日かかってしまいました。環境構築は手順化されていたのですが、トラブったのは主にNative Moduleを利用する libv8therubyracerrmagick などのおなじみの面々です。手順を作った時は、これらのgemのインストールに必要なライブラリを brew install で最新バージョンをインストールするだけでよかったのですが、時が経ちライブラリの最新バージョンが更新されていたことが主な原因でした。この状況はいかんな〜と思い、Railsアプリケーションの開発環境もDockerにすることにしました。

本記事では、Railsアプリケーションの開発環境をDockerにするときに検討したことや問題点などを書いています。なお、本記事で紹介するDockerfileやdocker-compose.ymlは開発環境用なので、本番環境の参考にはしないでください。

アジェンダ

  • セットアップ
  • デバッグ
  • Tips
  • 成果物

セットアップ

Railsアプリケーションのセットアップには、主に bundle installrake コマンドの実行があります。これらそれぞれをタイミングで行うのが良いか検討しましました。

bundle install

結論としてはDockerfileに含めてbuildの段階で実行することにしました。理由は、Gemfileの変更がそれほど頻繁ではないこと、Gemfileを変更しなければbuild時にキャッシュが効くので時間がかからないことなどがあります。また、コンテナ起動後にGemfileを変更した場合は、 docker-compose exec rails /bin/bash でコンテナのシェルを起動し、 bundle install することでgemの更新ができます。

もう一つのアイディアとして、Data Volume Container を作成し、そこにgemをインストールする方法を試しました。このメリットは、volumeを削除しない限りインストールしたgemのデータを再利用できるため、Gemfileを頻繁に更新してもbuildが素早くできるということです。ただし起動時にはgemがインストールされている必要があるので、どこかのタイミングで docker-compose run --rm rails bundle install などを実行する必要があります。起動時にcommandなどで bundle install するのは、起動が遅くなるのがいまいちでした。しかしこの方法には、DockerのVolumeの読み込みが非常に遅いという大きな問題があったので諦めました。この問題についてはDocker for Macが遅い問題をdocker-syncで解決する - Cluex Developersブログが詳しいです。

採用はしませんでしたが、Data Volume Container を使う設定はこんなかんじです。不要な箇所は省いています。docker-compose.yml の version:3 から volumes_from がなくなっていて、代わりの書き方を調べるのに苦労しました。いまだにしっくりきません。もしかして間違っているかも。

docker-compose.yml

version: "3"
services:
  rails:
    build: .
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    volumes:
      - .:/myapp
      - bundle-volume:/myapp/vendor/bundle
    ports:
      - 3000:3000
  datastore:
    build:
      context: .
      dockerfile: Dockerfile-datastore
    volumes:
      - bundle-volume:/myapp/vendor/bundle
volumes:
  bundle-volume:

datastoreのイメージを作成するための Dockerfile。

Dockerfile-datastore

FROM busybox:latest

RUN mkdir -p /myapp/vendor/bundle
VOLUME /myapp/vendor/bundle

Data Volume Container は複数のコンテナ間でvolumeを共有するための方法です。私もまだ理解が怪しいので、詳しくはManage data in containers - Docker Documentationを参照してください。

rake コマンド

結論としては手動で docker-compose run --rm rails rake db:migrate などを実行することにしました。初期化用にスクリプトにまとめておいてもいいかもしれません。前述した通り、起動時のcommandに設定してしまうと、必要もないのにコマンドが実行され毎回の起動が遅くなるため、起動とは分けました。

デバッグ

次にデバッグの方法について説明します。

byebug, pry-byebug

コンテナ内で起動しているRailsアプリケーションに対しての、byebugpry-byebug を利用したデバッグは、コンテナに Attach することでできます。Attachしてコンテナの外から命令を送るために stdin_open: truetty: true の2つの設定が必要になります。

docker-compose.yml

version: "3"
services:
  rails:
    build: .
    command: /myapp/script/start.sh
    volumes:
      - .:/myapp
    ports:
      - 1234:1234 # debug port
      - 3000:3000
    stdin_open: true
    tty: true

これで docker-compose up -d を実行し、アプリケーションが起動した後に docker attach xxx_rails_1 を実行します。すると以下のようにdebug画面が表示されます。以下は byebug の例です。

[8, 17] in /myapp/app/controllers/messages_controller.rb
    8:     @per = params[:per] || 20
    9:     @page = params[:page] || 1
   10:     @message = Search::Message.new(search_params)
   11:     @messages = @message.matches.page(@page).per(@per)
   12:     byebug
=> 13:   end
   14:
   15:   # Message新規作成画面を表示
   16:   def new
   17:     @message = Message.new
(byebug) @per
20
(byebug)

Attach したコンテナから抜ける場合は、 Control + P, Q でコンテナを終了せずコンソールだけ抜けます。 Control + C をするとコンテナが終了してしまいます。

参考: Docker and ruby on rails – Dominik Burgdörfer – Medium

Intellij IDEA, RubyMine

Intellij IDEA や RubyMine でもデバッグ機能を使うことができます。まずはじめに「intellij docker rails debug」とかで検索するとRubyMine 2017.1 Help :: Docker - Creating a Docker Deployment run/debug configurationという、いかにもデバッグ実行ができそうなドキュメントが見つかるのですが、こちらはIntellijからDockerやDockerCopmoseを起動する設定で、デバッグではありませんでした。

よくよく調べると、Intellij IDEA や RubyMine のRubyのデバッグ機能は「Ruby remote debug」というIDEの機能と ruby-debug-ide という gem を使っているということが分かり、こちらの記事に UPDATED: Running the Rails debugger in a Docker container using RubyMine on a Mac にやり方がそのまま載っていました。

まず IDE の RUN > Edit Configuration... を開き、「+」ボタンから「Ruby remote debug」を追加します。設定は以下のとおりです。

Ruby remote debug

Gemfileに以下を追加します。

Gemfile

group :development, :test do
  # remote debug
  gem 'ruby-debug-ide'
  gem 'debase'
end

rdebug-ide で Railsサーバを起動します。

bundle exec rdebug-ide --host 0.0.0.0 --port 1234 --dispatcher-port 26162 -- /myapp/bin/rails s -b 0.0.0.0 -p 3000 -e development

以下のメッセージが表示されますので、この状態で先ほど作成した「Ruby remote debug」を実行します。

Fast Debugger (ruby-debug-ide 0.6.0, debase 0.2.1, file filtering is supported) listens on 0.0.0.0:1234

Rails serverが起動します。

Fast Debugger (ruby-debug-ide 0.6.0, debase 0.2.1, file filtering is supported) listens on 0.0.0.0:1234
=> Booting WEBrick
=> Rails 4.2.4 application starting in development on http://0.0.0.0:3000
=> Run `rails server -h` for more startup options
=> Ctrl-C to shutdown server

あとは、Intellijにブレイクポイントを設定して通常と同じデバッグができます。ただ、ステップインでソースコードが移動しないなど、完璧な動作ではありませんでした。

Ruby remote debug の トラブルシューティング

ブレイクポイントで止まらない

Cannot render console from 172.19.0.1! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255

このエラーが出力されている場合、config/environments/development.rb に以下を追加します。

config/environments/development.rb

config.web_console.whitelisted_ips = '0.0.0.0/0'

ブレイクポイントでNoMethodErrorが発生する

`undefined method '+' for nil:NilClass` 

Trace log
byebug (5.0.0) lib/byebug/processors/command_processor.rb:27:in `at_breakpoint'
byebug (5.0.0) lib/byebug/context.rb:78:in `at_breakpoint'
app/controllers/messages_controller.rb:16:in `new'

byebug のバージョンが古いことが問題でした。 byebug のバージョンを最新にします。

Tips

プロセスの起動ファイルが消えない場合がある

docker stop したときに、 tmp/pids/server.pid が削除されず、次に docker start した時にRailsアプリケーションが起動できないことがあります。雑な対応ですが、 docker start 時に実行するシェルに rm /myapp/tmp/pids/server.pid を1行追加して対処しました。

CircleCI 2.0 のホスト名の解決

docker-compose で depends_on したサービスのホスト名はサービス名でDNS解決するのですが、CircleCI 2.0 では 127.0.0.1 で解決するので設定に差異が生まれました。これはホスト名を環境変数にすることで対処しました。また、ホスト名のデフォルト値を 127.0.0.1 にすることで、Dockerを使わないローカル環境の起動もできるようにしています。

.circleci/config.yml

version: 2
jobs:
  build:
    working_directory: ~/my-app
    docker:
      - image: ruby:2.2.3
      - image: mysql:5.7.11
        environment:
          MYSQL_ROOT_PASSWORD: password
          TZ: JST
      - image: fingershock/dynamodb-local
以下略

config/database.yml

test:
  host: <%= ENV['RDS_HOSTNAME'] || '127.0.0.1' %>

成果物

最後に完成した Dockerfile と docker-compose.yml を紹介をします。Docker公式のドキュメントにRailsアプリケーション用のDockerfileとDockerComposeのサンプルがあるのでこれをベースとしました。

Quickstart: Compose and Rails - Docker Documentation

Dockerfile

FROM ruby:2.2.3

RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs

# aws-cliのインストール
RUN apt-get install -y python2.7-dev \
 && curl -O https://bootstrap.pypa.io/get-pip.py \
 && python get-pip.py \
 && pip install awscli 

RUN mkdir /myapp

WORKDIR /myapp

ENV BUNDLE_JOBS=4

ADD Gemfile /myapp/Gemfile
ADD Gemfile.lock /myapp/Gemfile.lock

RUN bundle install

Railsアプリケーションのセットアップに aws-cli が必要なので、追加してインストールしています。

docker-compose.yml

version: "3"

services:
  mysql:
    image: mysql:5.7.11
    ports:
      - 3306:3306
    environment:
      - MYSQL_ROOT_PASSWORD=password
  dynamodb:
    image: fingershock/dynamodb-local
    ports:
      - 8000:8000
  rails:
    build: .
    command: /myapp/script/start.sh
    environment:
      AWS_DEFAULT_REGION: ap-northeast-1
      AWS_ACCESS_KEY_ID: dummy
      AWS_SECRET_ACCESS_KEY: dummy
      DYNAMODB_ENDPOINT: http://dynamodb:8000
      RDS_HOSTNAME: mysql
    volumes:
      - .:/myapp
    ports:
      - 1234:1234 # debug port
      - 3000:3000
    depends_on:
      - mysql
      - dynamodb
    stdin_open: true
    tty: true

script/start.sh

#!/bin/bash

# プロセスが正常終了しなかったときに、 server.pid ファイルが残ることがあるので削除する。
rm /myapp/tmp/pids/server.pid

bundle exec rails s -p 3000 -b '0.0.0.0' -e development

script/debug.sh

#!/bin/bash

# プロセスが正常終了しなかったときに、 server.pid ファイルが残ることがあるので削除する。
rm /myapp/tmp/pids/server.pid

bundle exec rdebug-ide --host 0.0.0.0 --port 1234 --dispatcher-port 26162 -- /myapp/bin/rails s -b 0.0.0.0 -p 3000 -e development

さいごに

これでローカル環境にRubyすらなくてもRailsアプリケーションの開発ができるようになりました。開発環境の構築も10分くらいで済むでしょう。トレードオフとして開発者はある程度のDockerの知識が必要となり、慣れるまで1日くらいかかるかもしれません :)