By

Railsアプリケーション開発を完全にDocker化する

Degica のすべてのサービスは 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:

これらのファイルを使って、次のように実行すると、webdbspring のすべてのサービスが起動します。

# 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 経由で railsrake が実行できます。

$ docker-compose run spring spring rails console
$ docker-compose run spring spring rake -T

何がうれしいのか

御覧頂いたように、rails server は通常通り起動しており、 railsrake といった開発作業で使用するコマンドは docker-compose 経由で通常通り実行できます。
開発者の環境には dockerdocker-compose 、(Linux以外なら) docker-machine といった Docker ツール群さえホストにインストールしてあれば、あとは rails アプリケーション開発を行うために必要なツールはありません。ruby すらホストマシンにインストールする必要はありません。
これは新規メンバーが入った場合等の初期セットアップを著しく簡単にし、各開発者の開発環境の差を docker が吸収し、しかも本番環境に近いセットアップであるため、「開発環境では発生しない」問題の発生確率を低下させます。

便利のため、bin/spring に次のようなスクリプトを用意しておくとよいでしょう。

#!/usr/bin/env bash
docker-compose run spring spring $@

これで、bin/spring rails c とかできるようになりました。

何が起こっているのか

ここからは実装の説明です。
Dockerfile.developmentdocker-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 つのサービスを定義しています。
このうち webspring コンテナはカレントディレクトリを 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 による方法でも近い効果が得られます。しかしこの方法ではホストマシンに rubyspring をインストールする必要があります。

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")
一緒にユニークな決済サービスを作ってくれる Rails エンジニアを募集中です!
多国籍なメンバーと一緒に仕事をしてみませんか?詳細はこちらのリンクです:D