Docker + Rails開発環境Tips
普段なんとなくで設定していたのですが、今の所こんな感じでやるのがいいんじゃない?というのをそろそろまとめたくなったので書いてみました。 もっといい方法があれば教えてください〜。
bundle install の実行のたびにDockerイメージのビルドし直しになるのを回避する
Quickstart にあるような構成では、以下のように Dockerfile で bundle install する。
FROM ruby:2.5.3
RUN mkdir /app
WORKDIR /app
RUN gem install bundler
RUN bundle install
この構成はgem一式を含めてポータブルにするという意味では合理的なのだが、開発環境においてはそうでもない。なぜなら、開発環境ではGemfileの更新やgemのアップデート、Dockerfileの修正が頻繁に発生し、そのたびに各開発者のローカルマシンでのイメージのビルドが必要になってしまうからだ。
これを回避するには、イメージの外、つまりコンテナで bundle install を行って gem を保存すればよい。ただし、コンテナを破棄した際に gem も消えてしまうと困るので、キャッシュできる必要がある。そのためには以下の方法がある。
- bundle install 用のディレクトリをホストと共有し、そこに格納する
- gem 用の volume を用意し、そこに格納する
前者のほうがお手軽だが、後者のほうがディスクアクセス的に有利そうではある(未計測)。もしホストと共有したくないとか、パフォーマンス上問題になってきたら後者に乗り換える形でどうだろうか。
bundle install をコンテナで行い、そのディレクトリをホストと共有する
BUNDLE_PATH
でインストール先をホストと共有するディレクトリ(この場合は /app
配下の /app/vendor/bundle
)に指定。
FROM ruby:2.5.3
ENV APP_PATH=/app
ENV BUNDLE_PATH=$APP_PATH/vendor/bundle
RUN bundle config path $BUNDLE_PATH
なお、 Bundler は BUNDLE_PATH
, .bundle/config
の順で優先してパスの設定を読み込む。そのためどちらかを指定していれば問題ないが、まれに .bundle/config
の設定がないとRailsが require 'bundle/setup
したときに BUNDLE_PATH
を見てくれないことがあった。そのため上記の例では両方設定している(原因特定できておらず、環境要因かもしれないので要確認)。どの設定が効いているかは docker-compose run --rm app bundle config
で確認できる。
docker-compose.yml
は以下のように /app
配下をホストにマウントする。 vendor/bundle
を delegated
フラグ付きでマウントすると高速に同期できる(ただし、コンテナ→ホストへのWriteパフォーマンスが犠牲になる。bundler配下ではほぼ発生しないので問題なし)
version: '3'
services:
app:
build: .
command: ['./bin/rails', 's']
tty: true
stdin_open: true
ports:
- '3000:3000'
volumes:
# cached/delegated options for performance tuning
- .:/app:cached
- ./vendor/bundle:/app/vendor/bundle:delegated
- ./node_modules:/app/node_modules:delegated
gem 用の volume を用意し、そこに格納する
BUNDLE_PATH
はマウントされないところにしておく。
ENV APP_PATH=/app
ENV BUNDLE_PATH=/bundle
RUN mkdir $APP_PATH
WORKDIR $APP_PATH
そして docker-compose.yml
でボリュームを用意し紐付ける。
version: '3'
services:
app:
volumes:
- bundle:/bundle
volumes:
bundle:
# use default driver
binstub も考慮したい場合は、以下のようにすると良いらしい。これがなくても手動で binstub を生成する(そしてリポジトリに含める)ことは可能な気がするが、この方法の便利なところは entry point を定義しておくと docker-compose up
のたびに最新の状態に保てることだろう。そういう意味では、 bin/setup
を entry point に登録しておくと便利なのかもしれない。
https://medium.com/@jfroom/docker-compose-3-bundler-caching-in-dev-9ca1e49ac441
デバッガによる対話コンソールを有効にする
よくある docker-compose.yml
では、以下のように docker-compose up
でRailsサーバも一緒に起動するようになっている。
services:
app:
build: .
command: ['./bin/rails', 's', "-b", "0.0.0.0"]
この設定だと標準入力が閉じられているので、 binding.pry
や binding.irb
のような対話コンソールによるデバッグが行えない。従って以下の設定を追加する。
services:
app:
build: .
command: ['./bin/rails', 's', "-b", "0.0.0.0"]
tty: true
stdin_open: true
ただし、 docker-compose up
でコンテナを起動した状態では、デバッガが起動した内容のログを確認できても、対話コンソールにアクセスできない。Railsサーバのコンテナは docker-compose up
のプロセスの後ろにいるためのようだ。従って一工夫する必要がある。2通りの方法を紹介する。
Railsサーバのコンテナに attach する
docker attach
コマンドを使うと、起動中のコンテナの標準入出力に接続することができる。これをRailsサーバのコンテナに利用する。具体的には次の操作を行う。
- デバッガのブレークポイントを仕込んで、そこを通る処理を実行する(処理を通って対話コンソールが待機状態になっているかどうかは
docker-compose up
のログ、もしくはdocker-compose logs
でも確認できる) - 別のターミナルで
docker attach $(docker-compose ps -q app)
を行って接続。接続直後は何も出力されないので、エンターキーを押して対話コンソールが起動していることを確認すると良い - デバッグを行う
- 対話コンソールを終了する(
ctrl-d
やquit
などで) - コンテナへの接続を解除する。
ctrl-p ctrl-q
を連続して入力(デフォルトの設定。docker attach --detach-keys="<sequence>"
で変更可能)。うっかりctrl-c
を押してしまうとコンテナごと終了してしまうので注意
このように、Dockerを利用せずにローカルでサーバを起動する場合とは全く感覚が異なる。特にチームで作業する場合などでは、効率を上げるためにも bin/attach
のようなスクリプトを作っておくと良さそうだ。
#!/usr/bin/env bash
echo attach to $1 ...
docker attach --detach-keys="ctrl-c" $(docker-compose ps -q $1)
次のような起動スクリプトでさらにラップしても良い。
docker-compose up -d
bin/attach app
Railsサーバのコンテナだけ docker-compose up とは別に起動する
attach を利用する方法で以下のような懸念がある場合には、Railsサーバを独立したコンテナで起動すると便利だ。
bin/attach
のようなスクリプトを作っても、意識のアップデートが必要。Dockerに慣れていない場合にハードルがある- Railsサーバのコンテナに変更や試行錯誤が多く、再起動する機会が多い
up -d
で起動してstop app
⇒start app
で個別に再起動することはできるが、フォアグラウンドで起動、終了を制御したい
このためには、まず docker-compose.yml
で次のようにRailsサーバを起動しないようにする。ここではかわりに bin/update
を実行して環境の最新化を行うようにしている。
services:
app:
build: .
command: ["./bin/update"]
そして、Railsサーバは次のようなスクリプトで起動する(Makefileに入れるのが楽だ)。
docker-compose up -d
./bin/sleep_until_app_ready
docker-compose run --rm --service-ports app bin/rails s
注意点は —service-ports
オプション。 docker-compose.yml
で設定した ports が、 run
のときにはこのオプションがないと反映されない。
https://docs.docker.com/compose/reference/run/
The second difference is that the docker-compose run command does not create any of the ports specified in the service configuration. This prevents port collisions with already-open ports. If you do want the service’s ports to be created and mapped to the host, specify the --service-ports flag
途中で使っている ./bin/sleep_until_app_ready
は以下のような簡単なスクリプトで、 bin/update
が終了するのを待っている。
#!/usr/bin/env bash
while :; do
sleep 1
docker-compose exec db echo 'alive?' &>/dev/null
DB_UP=$?
docker-compose exec app echo 'alive?' &>/dev/null
APP_UP=$?
if [ $DB_UP -eq 0 ] && [ $APP_UP -eq 1 ]; then
break
else
echo -n "."
fi
done
echo "Ready for running app"
対話コンソールで利用されるページャ
ruby
イメージはデフォルトでは less を備えていないので、デバッガは ruby 実装の自前ページャを利用する。ローカルで動かしたときのように less で見たい場合は、インストールするように Dockerfile を修正すると良い。
Springサーバーを有効にする
spring-docker-example が参考になる。
基本的には以下のようにSpringサーバ用のコンテナを立てて、
services:
app:
build: .
volumes:
- .:/app:cached
depends_on:
- db
- spring
spring:
build: .
volumes:
- .:/app:cached
command: ["./bin/spring", "server"]
spring.sock
をコンテナ間で共有するだけ。そのために、 sock ファイルを tmp ディレクトリ配下(ホストにマウントして共有)に置くように、 config/spring_client.rb
(Springが起動時に読み込む)でその場所を教えることにする。環境変数なので、もちろん他の方法で指定しても良い。
ENV["SPRING_SOCKET"] = "tmp/spring.sock"
コンテナをまたいだ spring の自動立ち上げはできない(方法あったら教えてほしい)ので、適宜 depends_on: spring
を設定しておくと良い。
注意したい点として、 Springサーバのコンテナの環境変数を、Railsサーバのコンテナの環境変数と揃えておくこと。Springサーバのコンテナが異なる環境変数を利用し、それを元に各種定数を初期化してキャッシュすると、Railsサーバは異なる環境変数を元にした定数を参照することになる。例えば database.yml
にERBで環境変数を埋め込んでいる場合に、意図通りに設定されない可能性がある。
default: &default
host: <%= ENV.fetch("DATABASE_HOST") { 'localhost' } %>
同様の理由で、マウントするディレクトリやボリュームも同じ条件にしておくのが安全。Spring経由での Dir.glob
の結果が異なることがあるため。
なお、 spring-docker-example の例では、ホストとコンテナの両方で同じ spring.sock
を共有し、さらに SPRING_SERVER_COMMAND='docker-compose up spring'
を設定したりPIDのネームスペースをホストにしたりして、ホストからSpringを利用して bin/rails c
を行うことができるようにしている(というか、そもそもホストにRubyとSpringを入れて起動するのを前提にしているようにみえる)。ホストとの共有はまだ試したことがなくて、きっと spring コンテナの環境での Rails コンソールが立ち上がると思うのだが、本当に macOS 上の環境に依存しない( native extension の違いとか、 webpacker が実行する yarn check --integrity
の結果とかの影響を受けない)のか気になるところ。今のところはホストからの利用はせずにコンテナ上で完結するように設定しているが、そのうち試したい。