graphql-ruby + Railsで簡易なクエリとミューテーションが使えるGraphQL APIを実装する

会社でGraphQLのハンズオンがあったのをきっかけに、最近はGraphQLのサーバ側実装をちょっと触っています。

graphql-rubyを使うと、RubyでGraphQL APIを実装することができます。今回はRailsでGraphQLのクエリミューテーションを実装してみました。

graphql-ruby使用時のRailsプロジェクトにおけるファイル/ディレクトリ構成

rails generate graphql:install すると、ジェネレータが app 配下に次のようなディレクトリ構成を作ります。

app/controllers
└── graphql_controller.rb
app/graphql
├── app_schema.rb
├── mutations
└── types
    ├── mutation_type.rb
    └── query_type.rb

また、ジェネレータは config/routes に次のルーティングを追加します。GraphQL APIへのあらゆるHTTPリクエストは次のエンドポイントへPOSTメソッドで送信することとなります。

post "/graphql", to: "graphql#execute"

今回の前提条件

このエントリでは、RailsはAPIモードを使います。また、今回は次のようなデータ構成になっていることとします。

f:id:kymmt90:20170726233709p:plain

User の認証機構は作りません。実際はなんらかの認証があって、クエリ実行時に context というオブジェクトを通じて current_user を扱うことになります。

今回のゴール

今回は、次のようなクエリとミューテーションをAPIへリクエストできるようにします。

まず、クエリです。これはサーバ側のデータを取得するタイプのリクエストで、いまは引数 email で指定した user に関するデータを取得しようとしています。

{
  user(email: "foo@example.com") {
    email
    article {
      edges {
        node {
          title
          body
        }
      }
    }
  }
}

この場合、次のようなレスポンスが返ります。

{
  "data": {
    "user": {
      "email": "foo@example.com",
      "article": {
        "edges": [
          {
            "node": {
              "title": "Title 1",
              "body": "this is the body"
            }
          },
          {
            "node": {
              "title": "Title 2",
              "body": "this is the body 2"
            }
          }
        ]
      }
    }
  }
}

次にミューテーションです。これはサーバ側のデータを変更します。

{
  createArticle(article: {user_email: "foo@example.com", title: "Another test", body: "This is another test"}) {
    article {
      title
      body
    }
  }
}

この場合、article が1件増えて、次のようなレスポンスが返ります。

{
  "data": {
    "user": {
      "email": "foo@example.com",
      "article": {
        "edges": [
          {
            "node": {
              "title": "Title 1",
              "body": "this is the body"
            }
          },
          {
            "node": {
              "title": "Title 2",
              "body": "this is the body 2"
            }
          },
          {
            "node": {
              "title": "Another test",
              "body": "This is another test"
            }
          }
        ]
      }
    }
  }
}

実装手順

利用するデータの型を書く

リクエストやレスポンスに出てくるデータの型 app/graphql/types 配下の user_type.rbarticle_type.rb にGraphQL APIで利用するデータの書きます。

# app/graphql/types/user_type.rb
Types::UserType = GraphQL::ObjectType.defind do
  name 'User'

  field :email, !types.String
  connection :articles, Types::ArticleType.connection_type
end

# app/graphql/types/article_type.rb
Types::ArticleType = GraphQL::ObjectType.define do
  name 'Article'

  field :title, !types.String
  field :body, !types.String
  field :user, Types::UserType
end

field は型が持つ属性であり、名前とスカラー型(types.String, types.Int, types.ID など)を指定します。また ! で非nullであることを指定します。

Types::UserTypearticlesconnection というヘルパを使っています。これはRelay由来のconnectionというページネーションを扱うための仕組みを使えるようにしてくれるものです。connectionによるページネーションの詳細については次のページを見てください。

今回はクエリのルート階層に書くフィールドは user になりますが、これは name 'Query' を宣言している型に書きます。

# app/graphql/types/query_type.rb
Types::QueryType = GraphQL::ObjectType.define do
  name 'Query'

  field :user do
    type Types::UserType
    argument :email, !types.String
    resolve ->(obj, args, ctx) {
      User.find_by(email: args['email'])
    }
  end
end

ここでは、argument にクエリで指定する引数を書いています。また、resolveuser クエリが投げられたときの User レコードの取得方法をラムダ式で書いています。

ここまでで、ゴールとしていたクエリをリクエストできるようになりました。

ミューテーションと入力データの型を書く

次にミューテーションを書きます。まずは name 'Mutation' を宣言している型にフィールドとしてミューテーションを書いてきます。

# app/graphql/types/mutation_type.rb
Types::MutationType = GraphQL::ObjectType.define do
  name 'Mutation'

  field :createArticle, Types::ArticleType do
    argument :article, Types::ArticleInputType

    resolve ->(o, args, c) {
      user = User.find_by!(email: args[:article][:user_email])
      user.articles.create!(title: args[:article][:title], body: args[:article][:body])
    }
  end
end

ここで2点ほどポイントがあります。

  • argument の型が Types::ArticleInputType である
  • resolve でレコードを作成している

Types::ArticleInputType は別途定義している次のような型です。

# app/grpahql/types/article_input_type.rb
Types::ArticleInputType = GraphQL::InputObjectType.define do
  name 'ArticleInputType'

  argument :user_email, !types.String do
    description 'Email address of the user'
  end

  argument :title, !types.String do
    description 'Title of the article'
  end

  argument :body, types.String do
    description 'Body of the article'
  end
end

この型を引数とすることで、次のような Types::ArticleInputType 型の引数としてミューテーションに作成したい article のデータを渡すことができます。この方法だと型の再利用性が高まります。

createArticle(article: {user_email: "foo@example.com", title: "Another test", body: "This is another test"}) {
  # ...
}

resolve では、ミューテーションの実際の処理として、対象ユーザの関連レコード article を作成しています。

これでゴールとしていたミューテーションもリクエストできるようになりました。

サンプルコード

素振り用に実装した上記コードを次の場所に置いています。

github.com

Yokohama.rb Monthly Meetup #82に参加した

2017-07-08(土)のYokohama.rb Monthly Meetup #82参加メモです。

yokohamarb.doorkeeper.jp

Rubyレシピブック

レシピ257から260まででした*1。259, 260についてメモ。

259: リモートホストが稼働していることを確認する

リモートホストが動いているかを確認するためにTCP接続を試みるメソッドについてのレシピです。

本文中では、TCPの7番ポートで受け付けるechoサービスに対して、TCPで接続を試みることでリモートホストが動いているかを確認しています。とはいえ、実際にやってみると、TCP7番を開けているサーバはほとんどなさそうでした。

コード例では timeout ライブラリによって導入される Kernel#timeout を使っています。このメソッドはRuby2.3からdeprecatedになっており、使うと次のメッセージが表示されます。実際には Timeout#timeout へのエイリアスになっているようです*2

Object#timeout is deprecated, use Timeout.timeout instead.

メッセージのとおり、現在は明示的にモジュール関数 Timeout.#timeout を使うのが望ましいでしょう。

Timeout.timeout do
  TCPSocket.open(host, service) do
    # pass
  end
end

260: 別プロセスのRubyオブジェクトにアクセスする

dRubyを使った分散オブジェクトの操作についてのレシピです。dRubyを実際に動かしてみたのは初めてだったのでおもしろかったです。次のようなことを話していました。

  • リモートのオブジェクトに対して呼び出したメソッドはサーバ側で実行される
    • クライアント/サーバ間ではMarshalを使ってやり取りしているが、ブロック内のコード(要はProc)はMarshalでシリアライズできないので、クライアント側で実行される
  • 2,3文字ぐらいの文字列を1000万要素ぐらい入れた配列をクライアントからサーバに送ろうとすると、なぜかクライアントが死ぬ…
  • dRubyを使ってカードゲームのUNOを作ると楽しそう

その他

以前書いた「DockerでRailsの開発環境を作る」ブログ記事を@miyohideさんが読んでいたということで、あらためて何をやったか紹介しました。

blog.kymmt.com

次のようなコメントをもらいました。

  • たとえばDBへの接続のためにIPアドレスは必要?
    • docker-compose.yml でつけたサービス名をそのまま使える
  • 本番環境はコンテナではない?
    • 今は開発環境だけ
      • エンジニアとデザイナの環境を統一して、うまく動かないときの原因切り分けを楽にしたかったのが最初のモチベーションのため
  • ホストからコンテナへのファイルコピーが遅くなってくるのでrsyncを使っている
    • 現状そこをネックに感じるレベルには達してなかった
  • コンテナが溜まってくるのはどうしてる?
    • コマンド実行は --rm をつけてやってもらう
    • こんな方法もあるそうです

また、@takeshyさんからDDD(ドメイン駆動設計)に基づいてRailsを使ったAPIサーバを作り直した事例について共有していただきました。

qiita.com

次のようなお話が聞けました。

  • ドメイン層は純粋なドメインロジックのコードだけになるのでわかりやすい
  • リポジトリ層はActive Recordを使わずに生SQLを書くことが多い
  • いかにimmutableな値オブジェクトに寄せるかがキモ
  • ルートエンティティ経由でドメインロジックを呼び出す
  • コントローラ層は params の処理ぐらいだけやってアプリケーション層に委譲
  • アプリケーション層はコントローラ層のアクションと一対一対応している
  • ドメインの処理を終わった時などにイベントを飛ばして、それを監視しているサブシステムが通知処理などを実行する
    • メインのドメインに通知処理などが入らない
    • ユーザ作成など、実行した処理がすべてイベントとして記録されるので、ログ活用やデバッグが楽になる

懇親会

次の結論に達した気がします。



次回はまだ未定のようですが、普段どおりであれば8月の第2土曜日に開催されると思います。

*1:258「LDAPで情報を得る」は飛ばしました

*2:ref: https://github.com/nahi/httpclient/issues/289

Yokohama.rb Monthly Meetup #81に参加した

2017-06-10(土)のYokohama.rb Monthly Meetup #81参加メモです。

yokohamarb.doorkeeper.jp

3月の#78も参加していたのにメモを書いていなかったので、久しぶり感あります。

Rubyレシピブック

レシピ254から256まで。HTTP, SMTP, FTPというプロトコル3連発でした。

254: HTTPクライアントをつくる

HTTPクライアントを一からつくるというわけではなく、open-uri, net/http, httpclientを使ってみるというレシピでした。rest-clientというクライアントライブラリがあるという話も聞けました。

github.com

あとはlibcurlラッパのtyphoeusとかがありますね。

github.com

255: メールを送信する

net/stmpを使って、素のRubyスクリプトでメールを送るというレシピでした。ふだんはAction Mailerばかりなので新鮮。

今回はメールの受信テストにMailCatcherを使っていましたが、Railsのときはletter_openerを使ったり、Action Mailerにもプレビューがあるという話をしていました。

256: FTPでファイルを送受信する

Ruby, net/ftpというライブラリも標準添付されています。@igrepさんにPure-FTPdというFTPサーバを立ててデモしてもらいました。

バイナリファイル、テキストファイルの送受信がプロトコルとして存在していて、そのまま Net::FTP#getbinaryfile のように対応するメソッドが存在しています。get という名前ですが、返り値はなくローカルファイルを保存するという仕様。

その他

Rubocopを導入するとき、とっかかりのルールはどうしたらよさそうかという話をしたところ、onkcopの設定を参考にしてみるとよさそうというお話を聞いたので、それを眺めたりしていました。

github.com

あとはこのエントリをもくもく書いたり。

blog.kymmt.com



次回は2017-07-08(土)です。

yokohamarb.doorkeeper.jp

Springが動くRails+MySQLなAPIサーバの開発環境をDocker Composeで作る

このあいだ、Rails+MySQLという構成のアプリケーション開発環境をDocker Composeで構築できるようにしました。

blog.kymmt.com

いろいろと理解が深まるにつれて、何点かクリアしたい問題が見えてきました。

  • Dockerfile で使うイメージ
    • ruby:2.4.1-onbuildRUN bundle config frozen 1 しているので、あとから Gemfile を更新して bundle install できない
  • bundle install に時間がかかる
    • Gemfile にgemを追加するたびに、すべてのgemのインストールが走る
  • rails, rake コマンドの立ち上がりが遅い
    • アプリケーションプリローダSpringのサーバが立ち上がっていないため

今回はこれらの問題をクリアして、さらにいい感じの開発環境を作ります。

Dockerfile で使うイメージ

前回作った Dockerfile では ruby:2.4.1-onbuild というイメージを使っていました。このイメージは ONBUILD 命令を使っており、このイメージを使った Dockerfile をビルドすることで ONBUILD に指定されたコマンドが自動で実行されるようになっています。Rubyの場合だと、Gemfile のイメージ内へのコピーや bundle install の実行のような決まりきったコマンドが ONBUILD として指定されているので、Dockerfile の作成を省力化できます。ruby:2.4.1-onbuildDockerfile は次のものです。

しかし、この ruby:2.4.1-onbuild では、RUN bundle config --global frozen 1 というコマンドを実行するようになっています。これは、イメージ内の Gemfile を変更して bundle install できないようにするものです*1

デプロイするような用途であればこれでもよいですが、今回は開発環境がほしいので、Gemfile は必要なときに都度変更してgemをインストールできるようにしたいです。また、onbuild タグがついたイメージは非推奨となっているようでした*2

そこで、ruby:2.4.1-onbuild イメージを使うのはやめて、ruby:2.4.1 イメージをベースに自分でもろもろの作業をやる Dockerfile になるように書き直しました。

FROM ruby:2.4.1

ENV APP_HOME /usr/src/app
RUN mkdir -p $APP_HOME
WORKDIR $APP_HOME

COPY Gemfile \
     Gemfile.lock \
     $APP_HOME/

ENV BUNDLE_GEMFILE=$APP_HOME/Gemfile \
    BUNDLE_JOBS=4
RUN bundle install

(※後述の[追記]も参照してください)

これで、コンテナ内の Gemfile をあとから編集しても、bundle install が実行できます。

bundle install に時間がかかる

現在、bundle install で入るgemの保存先はコンテナ内になっています。この場合、Gemfile にgemを追加してインストールしたいときはイメージを作り直す必要がありますが、これだとすべてのgemのインストールが走ってしまいます。

これを回避するために、gemの保存先をDocker Volumeに変更します。docker-compose.yml でgem保存用のVolumeを作り、APIサーバを動かすコンテナにマウントします。

version: '3'
services:
  # dbの設定...

  app:
    build: .
    command: bin/rails s -p 3000 -b "0.0.0.0"
    depends_on:
      - db
    ports:
      - "3000:3000"
    stdin_open: true
    tty: true
    volumes:
      - .:/usr/src/app
      - bundle_cache:/usr/local/bundle
volumes:
  bundle_cache:

bundle_cache がgem保存用のVolumeです。マウント先が /usr/local/bundle になっているのは、ベースイメージとしている ruby:2.4.1GEM_HOME/usr/local/bundle としているからです。

https://github.com/docker-library/ruby/blob/752c5f7cf44870ceae77134b346d20093053c370/2.4/Dockerfile#L63

これで、gemをVolumeへ保存できるようになりました。コンテナが終了したあともVolumeにgemが残ります。Gemfile にgemを追加して bundle install したいときは、次のようにコンテナを起動すればOKです。

$ docker-compose run app --rm bundle install

[2017-06-11 追記]bundle install する場所について

bundle installDockerfile に書くと、イメージのビルド時にインストールされたgemがイメージに入ります。この時点ではgemを保存するVolumeが設定されていないのですが、docker-compose コマンド経由でコンテナを起動すると、Volumeがgem保存先にマウントされます。このVolumeが上述の説明で使っている名前付きボリュームだと、マウント先に存在するファイルはVolumeへコピーされます*3

しかし、コンテナ起動前後でgemの保存の仕組みが変わるのはわかりにくいです。Dockerfile 内に bundle install を書くのではなく docker-compose run --rm app bundle install のように明示的にVolumeへgemをインストールする、という方法のほうがわかりやすそうです。

rails, rake コマンドの立ち上がりが遅い

RailsにはデフォルトでSpringというアプリケーションプリローダが入っています。Springを起動させておくと、rails, rake のようなコマンドの実行を高速化できます。現在、Springのことを考慮していないので、APIサーバとは別にSpring用のコンテナを立ち上げるようにします。

実際の作業内容では、この記事が参考になりました。ほとんどこの記事のとおりやっています。

tech.degica.com

version: '3'
services:
  # dbの設定...

  app: &app_base
    build: .
    command: bin/rails s -p 3000 -b "0.0.0.0"
    depends_on:
      - db
    ports:
      - "3000:3000"
    stdin_open: true
    tty: true
    volumes:
      - .:/usr/src/app
      - bundle_cache:/usr/local/bundle
  spring:
    <<: *app_base
    command: bin/spring server
    ports: []
volumes:
  bundle_cache:

bin 配下にある railsrake のようなbinstubにSpringの処理を差し込み、叩くコマンドを省略できるようにする機能があるので、それを実行しておきます。

$ docker-compose exec spring bundle exec spring binstub --all
* bin/rake: spring inserted
* bin/rails: spring inserted

docker-compose upappspring のコンテナを立ち上げたあと、実行中の spring コンテナ内でSpringを通じたコマンドの実行ができます。

$ docker-compose exec spring bin/rails console

[参考]spring コマンド実行時にエラーが出る場合

もし .bundle/config 内で BUNDLE_DISABLE_SHARED_GEMS: 1 という設定がある場合は削除する必要があります。この設定があると、Springを通じたコマンド実行時にうまくgemの場所を解決ができず次のエラーが出てしまいます。

Could not find rake-12.0.0 in any of the sources
Run `bundle install` to install missing gems.

あんまりないと思いますが、以前に bundle install --path vendor/bundle したときに自動で BUNDLE_DISABLE_SHARED_GEMS が設定に入っていてハマりました…

結果

Dockerfile は次のようになりました。

FROM ruby:2.4.1

ENV APP_HOME /usr/src/app
RUN mkdir -p $APP_HOME
WORKDIR $APP_HOME

COPY Gemfile \
     Gemfile.lock \
     $APP_HOME/

ENV BUNDLE_GEMFILE=$APP_HOME/Gemfile \
    BUNDLE_JOBS=4
RUN bundle install

docker-compose.yml は次のとおりです。

version: '3'
services:
  db:
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
    image: mysql:5.7
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql
  app: &app_base
    build: .
    command: bin/rails s -p 3000 -b "0.0.0.0"
    depends_on:
      - db
    ports:
      - "3000:3000"
    stdin_open: true
    tty: true
    volumes:
      - .:/usr/src/app
      - bundle_cache:/usr/local/bundle
  spring:
    <<: *app_base
    command: bin/spring server
    ports: []
volumes:
  bundle_cache:
  mysql_data:

Rails+MySQLなAPIサーバの開発環境をDocker Composeで作る

先日、"Quickstart: Compose and Rails"の手順にしたがいながら、Docker ComposeでRails+MySQLがとりあえず動くような環境を作りました。

blog.kymmt.com

今回は、RailsのAPIサーバの開発環境をもうちょっといい感じにDocker Composeで作ってみます。DBにはMySQL 5.7を使います。

ディレクトリ構成

コマンド rails new sample_app --api --db=mysql でRailsのプロジェクトツリーを作り、ルートに Dockerfiledocker-compose.yml を置きます。

.
├── Gemfile
├── Gemfile.lock
├── app
│   └── ...
├── config
│   ├── database.yml
│   └── ...
├── db
│   ├── migrate
│   │   └── ...
│   ├── seeds.rb
│   └── ...
├── Dockerfile
├── docker-compose.yml
└── ...

イメージの作成

最終的に立ち上げたいDockerコンテナは次のふたつです。

  • app
    • APIモードのRailsの実行環境
  • db
    • MySQL 5.7の実行環境

このうち、db は既存のMySQL公式イメージを使ってコンテナを立ち上げます。app は次の Dockerfile を書いてイメージを作ります。

FROM ruby:2.4.1-onbuild

ここでは、Ruby 2.4.1の公式イメージ*1、特に ruby:2.4.1-onbuild というイメージを使います。

Rubyの公式イメージは buildpack-deps というイメージをもとにしています。この buildpack-deps はRubyやPythonのライブラリのインストールに必要となりやすいライブラリをあらかじめインストールする便利なイメージです。

MySQLをRubyから利用するために、本来は libmysql-dev というライブラリをインストールする必要があります。しかし、このライブラリは buildpack-deps があらかじめインストールしているので、今回の Dockerfile には明示的に書かないでも大丈夫です。

このあと、本来はホストに存在するRailsアプリケーションのプロジェクトツリーを app イメージ内にコピーする必要がありますが、この処理を Dockerfile に書いていません。これは、app のもととなるイメージに ruby:2.4.1-onbuild を使っているからです。このイメージの Dockerfile はおおむね次のようになっており、処理が ONBUILD で記述されています。ruby:2.4.1-onbuild では Gemfile のコピーやBundlerでの依存gemインストール、プロジェクトツリーのコピーといった定型作業を ONBUILD で指定してあります。ONBUILD を持つイメージをもとに作った新たなイメージからコンテナをビルドしたあとに、ONBUILD で指定した処理が実行されるようになっています。

FROM ruby:2.4

RUN bundle config --global frozen 1

RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app

# このイメージをもとにしたイメージからコンテナをビルドしたあとに実行する
ONBUILD COPY Gemfile /usr/src/app/
ONBUILD COPY Gemfile.lock /usr/src/app/
ONBUILD RUN bundle install
ONBUILD COPY . /usr/src/app

ちなみに、もし buildpack-deps で入るもの以外のライブラリをインストールしたい場合は、今回書いたDockerfile に次のように追記する必要があります。

RUN apt-get update -qq && apt-get install -y \
    build-essential \
    nodejs \
 && rm -rf /var/lib/apt/lists/*

ここでは、Debianの公式パッケージのビルドに必要な build-essential をインストールしています。さらに && でつないでインストールしたいパッケージの名前(たとえば nodejs)を指定します。このあと、さらに rm -rf var/lib/apt/lists/* を実行していますが、これはベストプラクティスとされている方法であり、APTのキャッシュを削除することでイメージのサイズを削減しています。

Composeファイルの作成

appdb をコンテナとして起動するためにComposeファイルを作ります。

version: '3'
services:
  db:
    image: mysql:5.7
    volumes:
      - mysql_data:/var/lib/mysql
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
    ports:
      - "3306:3306"
  app:
    build: .
    command: bin/rails s -b "0.0.0.0"
    volumes:
      - .:/usr/src/app
    ports:
      - "3000:3000"
    depends_on:
      - db
volumes:
  mysql_data:

ファイルの末尾で volumes を指定し、Docker Engineがサポートする名前付きボリュームとして mysql_data を作成しています。ボリュームというのはコンテナ間で共有できるデータを保存する仕組みです。ボリュームはコンテナとは独立して作成するため、たとえそのボリュームを使う db コンテナが破棄されてもデータはホストに残ります。

今回のComposeファイルでは、名前付きボリュームとして mysql_data を作成し、db の設定にある volumesmysql_datadb/var/lib/mysql ディレクトリにマウントする形で利用しています。

app では depends_on という設定項目に db を指定しています。これによって、依存先のサービスをコンテナとして立ち上げてから、本サービスを立ち上げるようになります。

Railsのデータベース設定

Railsの config/database.yml で、開発/テスト環境のDBを次のとおり設定します。

default: &default
  adapter: mysql2
  encoding: utf8
  pool: 5
  username: root
  password:
  host: db

development:
  <<: *default
  database: sample_app_development

test:
  <<: *default
  database: sample_app_test

docker-compose.ymlapp の依存先として db を指定し、host: db と指定することで、コンテナ db へ接続することができます。

コンテナの立ち上げ

次の要領で appdb の各コンテナを立ち上げます。

  1. APIサーバで使うDBをセットアップ(DBの作成、テーブルの作成、初期データの投入など)する
  2. Docker Composeでコンテナ群を立ち上げる

コンテナ群を立ち上げる前にDBをセットアップしておかないと、APIサーバにリクエストがあったときにエラーが発生します。

次のコマンドでDBをセットアップします。

$ docker-compose run --rm app bin/rails db:setup

このコマンドによって、次のように処理が進みます。

  • app が依存する db のコンテナを先にイメージから立ち上げる
  • app のコンテナを Dockerfile から立ち上げる
  • app のなかのワーキングディレクトリ /usr/src/appbin/rails db:setup を実行し、DBのセットアップを実行する

これでAPIサーバを動かせるようになりました。最後に次のコマンドを叩き、appdb のコンテナを立ち上げると、RailsとMySQLが動きだします。

$ docker-compose up

curl http://localhost:3000/users/1 のようなリクエスト送信で疎通が取れます。また、ホストの Dockerfile を置いているディレクトリをコンテナ内の該当ディレクトリにマウントしているので、ホストでコードを編集すると直にコンテナ内に反映されます。

開発中に Gemfile を更新したときは次のコマンドでイメージをビルドし直します。

$ docker-compose build app

RSpecのテストは次の要領で実行できます。

$ docker-compose run --rm app bin/rspec

*1:Railsの公式イメージは存在しますが、今では非推奨となっています