Railsアプリケーション開発を完全にDocker化する
TweetDegica のすべてのサービスは Rails で開発しており、そのうちの一部は Docker を使用した本番環境にデプロイしています。しかし開発者個人の開発環境にはいまだに Docker を導入できていません。最も大きな障害は spring
を docker コンテナ内で上手く扱う方法が確立されていなかったことですが、この問題は docker-compose
を工夫して利用することで解決可能であることがわかりました。
ということで、今回は rails アプリケーションの開発環境を完全に docker 化する方法を紹介します。 完全に、というところがポイントです。この方法を使えば docker 以外のツールを一切ホストマシンにインストールせずに rails アプリの開発を行うことができます。
(ちなみに、弊社の本番環境は Amazon ECS (EC2 Container Service) を使った社内 PaaS によって管理されていますが、この話はまた近いうちに紹介したいと思います。)
ソースコード
以下で説明する内容のソースコードは GitHub で公開しています。ご自由にご利用ください。
https://github.com/degica/dockerized-rails-example
サーバープロセスの Docker 化
webプロセス (rails server
) や delayed_job、resque といったプロセス、MySQL、Redis等のDBプロセスを docker を使用して管理するのは難しくありません。
Docker 公式ページ にその方法が詳しく書かれていますので、そちらを参照してください。
spring の Docker 化
こっちが今回の主題です。
rails
アプリ開発において spring はもはや欠かせないツールですが、spring を docker 化された開発環境で適切に使用する方法は (僕が調べた範囲では) 確立されていません。
spring を Docker 内で素朴な方法で起動した場合、例えばホストマシンから spring を 使って rails console
を起動したくても ホストマシンから spring コンテナに接続する方法がありません。この問題を解決する方法はいくつか考えられますが、docker-compose を使ったシンプルな方法を紹介します。実装の詳細は上述のレポジトリを確認してください。ここでは重要な部分を抜粋して説明します。
まず Dockerfile
ですが、次のようなものを使います。
# Dockerfile.development
FROM ruby:2.3.1
RUN apt-get update && \\
apt-get install -y mysql-client nodejs --no-install-recommends && \\
rm -rf /var/lib/apt/lists/*
ARG APP_HOME
RUN mkdir -p $APP_HOME
WORKDIR $APP_HOME
ADD Gemfile $APP_HOME/
ADD Gemfile.lock $APP_HOME/
RUN bundle install
次に、docker-compose.yml
version: '2'
services:
web: &app_base
build:
context: .
dockerfile: Dockerfile.development
args:
- APP_HOME=${PWD}
ports:
- "3000:3000"
command: rails s -p 3000 -b 0.0.0.0
links:
- db
volumes:
- .:${PWD}
environment:
DATABASE_URL: mysql2://root:test123@db:3306
spring:
<<: *app_base
ports: []
command: spring server
stdin_open: true
tty: true
pid: host
environment:
DATABASE_URL: mysql2://root:test123@db:3306
SPRING_SOCKET: /tmp/spring/spring.sock
volumes:
- .:${PWD}
- spring:/tmp/spring
db:
image: mysql:5.6
environment:
MYSQL_ROOT_PASSWORD: test123
volumes:
spring:
これらのファイルを使って、次のように実行すると、web
、db
、spring
のすべてのサービスが起動します。
# DBの起動に時間かかるので先に起動しておく
$ docker-compose up -d db && sleep 5
# DBのセットアップ
$ docker-compose run web bin/setup
# サービスの起動
$ docker-compose up
この状態で DOCKER_HOST の 3000ポート(僕の環境では 192.168.99.100:3000
) にブラウザからアクセスすると rails のデフォルト画面が表示されるのを確認できます。また、次のように実行することで、 spring
経由で rails
、rake
が実行できます。
$ docker-compose run spring spring rails console
$ docker-compose run spring spring rake -T
何がうれしいのか
御覧頂いたように、rails server
は通常通り起動しており、 rails
や rake
といった開発作業で使用するコマンドは docker-compose
経由で通常通り実行できます。
開発者の環境には docker
、 docker-compose
、(Linux以外なら) docker-machine
といった Docker ツール群さえホストにインストールしてあれば、あとは rails
アプリケーション開発を行うために必要なツールはありません。ruby すらホストマシンにインストールする必要はありません。
これは新規メンバーが入った場合等の初期セットアップを著しく簡単にし、各開発者の開発環境の差を docker が吸収し、しかも本番環境に近いセットアップであるため、「開発環境では発生しない」問題の発生確率を低下させます。
便利のため、bin/spring
に次のようなスクリプトを用意しておくとよいでしょう。
#!/usr/bin/env bash
docker-compose run spring spring $@
これで、bin/spring rails c
とかできるようになりました。
何が起こっているのか
ここからは実装の説明です。
Dockerfile.development
と docker-compose.yml
内で複数のトリックを使っているので順に説明します。
Dockerfile.development
まず Dockerfile.development
ですが、アプリケーションコードは ADD
しません。アプリケーションコードは docker-compose.yml
で カレントディレクトリをマウントしています。
また、WORKDIR
を ホストのカレントパスと一致させることで spring
コンテナにホストのフルパスの引数を渡しても正しく動くような親切設計にしています。
docker-compose.yml
カレントディレクトリのマウント
docker-compose.yml
では 前述したように web
(rails server
)、spring
(spring server
)、 db
(MySQL) の 3 つのサービスを定義しています。
このうち web
と spring
コンテナはカレントディレクトリを Dockerfile.development
により docker build
し、ホストのカレントディレクトリを コンテナの WORKDIR
にマウントしています。このようにマウントすることでホスト側のファイル変更を検知することによる自動リロードが有効になります。
spring ソケットのコンテナ間共有
docker-compose.yml
で最も特徴的なのは、spring
ボリュームを定義し、spring
コンテナがこのボリュームを /tmp/spring
にマウントしていることです。この spring
ボリュームはコンテナ間で共有されるもので、ホストからは見ることができません。
spring
はクライアントと通信を行うための UNIX ドメインソケットファイルを作成しますが、このファイルの保存先を SPRING_SOCKET: /tmp/spring/spring.sock
という環境変数によって spring
ボリューム内に保存されるよう指定しています。
さて、spring
ボリュームをマウントしているコンテナはこの spring.sock
を通じて spring server
と通信することができます。つまり、docker-compose run spring spring rails c
を実行すると、新しく起動した spring(クライアント) コンテナは /tmp/spring/spring.sock
を通じて spring server
コンテナに rails c
コマンドを送信することができます。
別解
今回紹介した方法は唯一の方法ではなく、いくつもの別解が存在します。
コンテナの spring ソケットをホストと共有する (Linux only)
もしチームの開発者が全員 Linux 使いであれば、jonleighton/spring-docker-example による方法でも近い効果が得られます。しかしこの方法ではホストマシンに ruby
と spring
をインストールする必要があります。
docker exec
を使う
<Update>
見落としてました。docker-compose は 1.7 から exec
サブコマンドをサポートしています。なので docker-compose exec
が動作します。もし、他のコンテナから spring
ソケットを参照しないならボリュームのマウントも省略可能です。
</Update>
docker-compose up
までは本記事で紹介した方法と同じですが、spring
の実行に docker exec
を使って spring server
コンテナ内で spring
コマンドを実行する方法も考えられます。
本記事時点の docker-compose
(1.7.1) は docker exec
をサポートしていないため、起動中の spring
コンテナ ID を取得して spring
を実行するスクリプトを自前で作らなければなりません。
Appendix: Emacs + rspec-mode
僕は Emacs (Spacemacs) を使っていますが、以下のような設定を追加したら emacs 内から rspec-mode
を動かすことができました。
改良の余地があると思うので、もっといいアイデアを発見したらぜひ教えてください。
;; init.el
(setq rspec-use-rake-when-possible nil)
(setq rspec-use-bundler-when-possible nil)
(setq rspec-use-spring-when-possible nil)
(setq rspec-spec-command "docker-compose run spring spring rspec")