Ruby
IntelliJ
RubyOnRails
docker

MacのIntelliJでDocker内のRailsをデバッグする

はじめに

ずっとやり方わからなかったんだけど最近表題のやり方が分かったので共有します。
説明用にサンプルプロジェクト作った1ので、これで説明します。

また、IntelliJから直接デバッグする方法と、Dockerをリモートサーバとしてデバッグする方法と2つ説明します。前者で全てうまくいけばそれでいいと思うんですが、できない場合は後者と言う感じで。

あとRubyMineも大体同じだと思う。Mac以外も大体同じだと思うけど、"Dockerをリモートサーバとしてデバッグする方法"はちょっと違うので注意。
使ったIntelliJのバージョンは2017.3.4。

プロジェクトに必要な準備

この次の章からサンプルプロジェクトを使って説明しますが、その前に読者の方が自分のプロジェクトでやる時に必要な準備について説明しておきます。
やるべきことは1つで、Gemfileにruby-debug-ideとdebaseを追加してインストールしておきます。

あとはbyebug系使ってるとうまく動かないことがあるっぽいので、使わないならGemfileから削除しておくことをおすすめします。
それ以外は多分いじらなくてもこのサンプルプロジェクトを使った説明と同じ手順で動くと思いますが、動かない場合はサンプルプロジェクトと違うところを見比べながら原因を探ると良いかと思います。重要なファイルは以下の3つでしょう。

  • Dockerfile
  • docker-compose.yml
  • Gemfile

あと最後の方にトラプルシューティングの章も設けたので必要があれば参照してください。

それではサンプルプロジェクトを使って説明していきます。

サンプルプロジェクトのIntelliJ設定

何はともあれサンプルプロジェクトのコンテナが動くようにする。

$ git clone git@github.com:takc923/rails-docker-compose-rubymine-example.git
$ cd rails-docker-compose-rubymine-example
$ cp .env.development .env
$ docker-compose up -d
$ docker-compose exec web bundle exec rake db:migrate
$ docker-compose stop web

次にIntelliJでのプロジェクト設定を行っていく。
↓のImport Projectから↑のディレクトリ開いてあとはデフォルトでOK押していってプロジェクトを開く。

スクリーンショット 2018-03-13 22.59.16.png

Project Structureを開く。
スクリーンショット 2018-03-11 11.22.44.png

Project → Project SDKのNew → Ruby SDK → New remote

スクリーンショット 2018-03-11 11.25.02.png
スクリーンショット 2018-03-11 11.25.37.png

Docker composeを選択し、ServiceをwebにセットしてOK。OKを押すと、docker-compose upってウィンドウと設定画面が交互に出るバグっぽい挙動をするが、落ち着くまで待つ。

スクリーンショット 2018-03-11 11.27.09.png

今作ったSDKがセットされていることを確認してOK

スクリーンショット 2018-03-11 11.30.09.png

そしたらもう一度Project Structureを開き、Modules → rails-docker-compose-rubymine-example → Ruby SDK and Gemsでさっき作ったSDKを選択しOK。ここら辺でもdocker-compose upのウィンドウでたりするかもしれないけど同様に落ち着くまで待つ。

スクリーンショット 2018-03-11 11.33.20.png

多分Indexingが走るので、落ち着くまで放置。これで必要な準備が整った。
”はじめに” で説明したようにこの次では"IntelliJから直接デバッグする"方法と、"Dockerをリモートサーバとしてデバッグする"方法とを説明する。

IntelliJから直接デバッグする

普通にRailsのRun configuration作ってデバッグ実行すれば良い。

スクリーンショット 2018-03-11 11.43.02.png
スクリーンショット 2018-03-11 11.43.18.png

これでブレイクポイントつけたりできる。
テストの実行もDockerじゃない時と同じように、テストファイルを右クリックからdebugとかでできる。

スクリーンショット 2018-03-11 11.46.54.png
スクリーンショット 2018-03-11 11.48.07.png

rake taskも普通に行ける。

スクリーンショット 2018-03-11 11.49.47.png

Dockerをリモートサーバとしてデバッグする

上記のやり方で済むならそれで良い。上記のやり方だとうまくいかない場合、すごく遅い場合、複数同時に実行したい場合2などは本項のやり方を試すと良い。
ただしこの方法だと僕の環境だとrails serverは何故かこの方法だとブレイクポイントで止まってくれなかった...

やり方は、例えばrake hoge:echoをデバッグしたい場合、以下のコマンドを実行(別のものをデバッグしたい場合はコマンドの最後のrakeコマンドのところだけ書き換える。フルパスじゃないと動かないっぽいので注意。)

$ docker-compose run --rm -p 1234:1234 web \
  env IDE_PROCESS_DISPATCHER='docker.for.mac.host.internal:26162' \
  rdebug-ide --disable-int-handler \
  --evaluation-timeout 10 \
  --rubymine-protocol-extensions \
  --port 1234 \
  --host 0.0.0.0 -- /usr/local/bundle/bin/rake hoge:echo

そうするとデバッガの接続を待ち受ける。

で、IntelliJ側でRuby remote debugのRun configurationを作る。普通にRuby remote debugのRun configurationを作って、Remote root folderとLocal root folderを正しく入力する
スクリーンショット 2018-03-11 13.06.02.png
スクリーンショット 2018-03-11 13.08.25.png

この設定でデバッグ実行すると、IntelliJのデバッガがdockerのプロセスに接続しに行って、デバッグできる。

Mac以外の場合は、試してないけど実行コマンドを以下のように変えたら多分いける(env...の部分がなくなって--dispatcher-port 26162を追加)

$ docker-compose run --rm -p 1234:1234 web \
  rdebug-ide --disable-int-handler \
  --evaluation-timeout 10 \
  --rubymine-protocol-extensions \
  --port 1234 \
  --dispatcher-port 26162 \
  --host 0.0.0.0 -- /usr/local/bundle/bin/rake hoge:echo

トラブルシューティング

"IntelliJから直接デバッグする"でデバッグ実行できない その1

ぼくの環境で起きたんですが、rakeタスクを実行しようとしてこんなエラーが出る場合がありました。

/usr/local/bin/ruby -e at_exit{sleep(1)};$$stdout.sync=true;$$stderr.sync=true;load($$0=ARGV.shift) /opt/project/vendor/bundle/ruby/2.4.0/gems/ruby-debug-ide-0.6.1/bin/rdebug-ide --disable-int-handler --evaluation-timeout 10 --evaluation-control --time-limit 100 --memory-limit 0 --rubymine-protocol-extensions --port 1234 --host 0.0.0.0 --dispatcher-port 26168 -- /opt/project/vendor/bundle/ruby/2.4.0/bin/rails hoge:echo
Fast Debugger (ruby-debug-ide 0.6.1, debase 0.2.2, file filtering is supported) listens on 0.0.0.0:1234
Fast Debugger (ruby-debug-ide 0.6.1, debase 0.2.2, file filtering is supported) listens on 0.0.0.0:41525
1: connection failed(1)
Exception: Connection refused - connect(2) for "172.19.0.1" port 26168
    /opt/project/vendor/bundle/ruby/2.4.0/gems/ruby-debug-ide-0.6.1/lib/ruby-debug-ide.rb:153:in `initialize'
    /opt/project/vendor/bundle/ruby/2.4.0/gems/ruby-debug-ide-0.6.1/lib/ruby-debug-ide.rb:153:in `open'
    /opt/project/vendor/bundle/ruby/2.4.0/gems/ruby-debug-ide-0.6.1/lib/ruby-debug-ide.rb:153:in `block in notify_dispatcher'
    /opt/project/vendor/bundle/ruby/2.4.0/gems/ruby-debug-ide-0.6.1/lib/ruby-debug-ide.rb:151:in `times'
    /opt/project/vendor/bundle/ruby/2.4.0/gems/ruby-debug-ide-0.6.1/lib/ruby-debug-ide.rb:151:in `notify_dispatcher'
    /opt/project/vendor/bundle/ruby/2.4.0/gems/ruby-debug-ide-0.6.1/lib/ruby-debug-ide.rb:116:in `block in start_control'
1: connection failed(2)
Exception: Connection refused - connect(2) for "172.19.0.1" port 26168
    /opt/project/vendor/bundle/ruby/2.4.0/gems/ruby-debug-ide-0.6.1/lib/ruby-debug-ide.rb:153:in `initialize'
    /opt/project/vendor/bundle/ruby/2.4.0/gems/ruby-debug-ide-0.6.1/lib/ruby-debug-ide.rb:153:in `open'
    /opt/project/vendor/bundle/ruby/2.4.0/gems/ruby-debug-ide-0.6.1/lib/ruby-debug-ide.rb:153:in `block in notify_dispatcher'
    /opt/project/vendor/bundle/ruby/2.4.0/gems/ruby-debug-ide-0.6.1/lib/ruby-debug-ide.rb:151:in `times'
    /opt/project/vendor/bundle/ruby/2.4.0/gems/ruby-debug-ide-0.6.1/lib/ruby-debug-ide.rb:151:in `notify_dispatcher'
    /opt/project/vendor/bundle/ruby/2.4.0/gems/ruby-debug-ide-0.6.1/lib/ruby-debug-ide.rb:116:in `block in start_control'
1: connection failed(3)
Exception: Connection refused - connect(2) for "172.19.0.1" port 26168
    /opt/project/vendor/bundle/ruby/2.4.0/gems/ruby-debug-ide-0.6.1/lib/ruby-debug-ide.rb:153:in `initialize'
    /opt/project/vendor/bundle/ruby/2.4.0/gems/ruby-debug-ide-0.6.1/lib/ruby-debug-ide.rb:153:in `open'
    /opt/project/vendor/bundle/ruby/2.4.0/gems/ruby-debug-ide-0.6.1/lib/ruby-debug-ide.rb:153:in `block in notify_dispatcher'
    /opt/project/vendor/bundle/ruby/2.4.0/gems/ruby-debug-ide-0.6.1/lib/ruby-debug-ide.rb:151:in `times'
    /opt/project/vendor/bundle/ruby/2.4.0/gems/ruby-debug-ide-0.6.1/lib/ruby-debug-ide.rb:151:in `notify_dispatcher'
    /opt/project/vendor/bundle/ruby/2.4.0/gems/ruby-debug-ide-0.6.1/lib/ruby-debug-ide.rb:116:in `block in start_control'

これ、ぼくの理解で何が起きてるか解説しようとすると、デバッグとMacのdockerの仕組みを知る必要があります。デバッグは多分以下のような仕組みで実行されてます。

スクリーンショット 2018-03-12 23.42.02.png

ですが、Macの場合、Dockerが軽量Linux内で実行されているので、間に1層増えます。

スクリーンショット 2018-03-12 23.42.08.png

この時containerがhostにつなぎに行こうとするんですが、containerは1234portに繋ぎに来たIPアドレスの26162portめがけてリクエストを飛ばすが、それはmoby linuxのIPアドレスになっていてhostにまで届かない、ということで上記のようなエラーになってるようなんです。
で、rdebug-ideのオプションを見ても、hostに繋ぎに行く時のIPアドレスまで指定できなさそうだったんですが、rdebug-ideのソースコードを読んでみるとオプションとは別に環境変数からIPアドレスとportを取得しているのを見つけたので、"Dockerをリモートサーバとしてデバッグする"時のコマンドが... env IDE_PROCESS_DISPATCHER='docker.for.mac.host.internal:26162' rdebug-ide ...となってるわけです。で、仕組みが分かったところでどうしたらいいんだってなると、今の所"IntelliJから直接デバッグする"を諦めて"Dockerをリモートサーバとしてデバッグする"で妥協する以外わからないです。
あとじゃあなんで他は上手くいくのか、っていうのもよくわかんなかったです。

参考: https://docs.docker.com/docker-for-mac/networking/#known-limitations-use-cases-and-workarounds

"IntelliJから直接デバッグする"でデバッグ実行できない その2

こんなエラーになる場合

/usr/local/bin/ruby -e at_exit{sleep(1)};$$stdout.sync=true;$$stderr.sync=true;load($$0=ARGV.shift) /opt/project/vendor/bundle/ruby/2.2.0/gems/ruby-debug-ide-0.6.0/bin/rdebug-ide --disable-int-handler --evaluation-timeout 10 --evaluation-control --time-limit 100 --memory-limit 0 --rubymine-protocol-extensions --port 1234 --host 0.0.0.0 --dispatcher-port 26168 -- /opt/project/bin/rails server -b 0.0.0.0 -p 3000 -e development
Using ruby-debug-base 0.2.2
Usage: rdebug-ide is supposed to be called from RDT, NetBeans, RubyMine, or
       the IntelliJ IDEA Ruby plugin.  The command line interface to
       ruby-debug is rdebug.

Options:
    -h, --host HOST                  Host name used for remote debugging
    -p, --port PORT                  Port used for remote debugging
        --dispatcher-port PORT       Port used for multi-process debugging dispatcher
        --evaluation-timeout TIMEOUT evaluation timeout in seconds (default: 10)
        --stop                       stop when the script is loaded
    -x, --trace                      turn on line tracing
    -l, --load-mode                  load mode (experimental)
    -d, --debug                      Debug self - prints information for debugging ruby-debug itself
        --xml-debug                  Debug self - sends information <message>s for debugging ruby-debug itself
    -I, --include PATH               Add PATH to $LOAD_PATH
        --keep-frame-binding         Keep frame bindings
        --disable-int-handler        Disables interrupt signal handler
        --rubymine-protocol-extensions
                                     Enable all RubyMine-specific incompatible protocol extensions
        --catchpoint-deleted-event   Enable chatchpointDeleted event
        --value-as-nested-element    Allow to pass variable's value as nested element instead of attribute

Common options:
    -v, --version                    Show version

invalid option: --evaluation-control

Process finished with exit code 1

ruby-debug-ideのバージョンが古いので0.6.1にあげる。

ブレイクポイントでエラーになる

ブレイクポイントでこんなエラーが出る場合

NoMethodError (undefined method `+' for nil:NilClass):
  app/controllers/visitors_controller.rb:3:in `index'


  Rendered /usr/local/bundle/gems/actionpack-4.2.5/lib/action_dispatch/middleware/templates/rescues/_source.erb (29.6ms)
  Rendered /usr/local/bundle/gems/actionpack-4.2.5/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb (6.9ms)
  Rendered /usr/local/bundle/gems/actionpack-4.2.5/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb (25.3ms)
  Rendered /usr/local/bundle/gems/actionpack-4.2.5/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb within rescues/layout (104.0ms)

byebug系とrdebug-ideの相性が悪いっぽいので、byebug系のgem(特にpry-byebug)をGemfileから削除する。


  1. 人が作ったのをちょっと手直ししただけだけど。 

  2. "IntelliJから直接デバッグする"方法だと、固定でportを取るので2つ目を実行しようとするとport取り合って失敗する。