Dockerコンテナからローカルで動いているRailsにアクセスする

Docker for Macは遅い

のでdocker-syncを使っていたんだけど、時々暴走してMacの電池残量を全力で削ってくれたり、 なんらかの拍子で同期が取れなくてコンテナにローカルの変更が反映されなかったりで結構困ることが多くなってきた。

特にコンテナにローカルの変更が反映されないのはかなり困っていて、時間短縮のために入れたはずのdocker-syncのために時間を割くといったよくわからない状況に陥りつつあったので、Railsだけはローカルで動かしてみることにした。

結論から言うとローカルで動かすこと自体はそこまで問題ではなかったけど、docker-composeで立てている残りのコンテナについて少々つまづきどころがあったので記す。

Precondition

具体例

例えばこんな感じのdocker-compose.ymlを定義していたとする。app、worker、DB、Redisが乗ったありがちな構成。

appはRailsで、workerはNode.jsで動くことを想定している。 また、appとworkerは相互にHTTPリクエストで通信を行うとする。

version: '2'
services:
  db:
    image: postgres:9.4
    volumes:
      - postgresql:/var/lib/postgresql/data
  redis:
    image: redis:3.2
  app:
    build: .
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    ports:
      - "3000:3000"
    depends_on:
      - worker
      - db
      - redis
  worker:
    build: ../worker/.
    command: npm start

これで$ docker-compose upを打つと、appとworkerのコンテナが作られ、それぞれのコマンドが実行され、動き始める。

depends_onでサービス名を明示的に指定しているため、appからはworker、workerからはappというホスト名で名前解決ができる。

さて、このコンテナ群のうち、appのみをローカルで動かすことを考える。

特定のコンテナのみをdocker-compose upで起動する

実はこれ自体はそんなに難しいことではない。docker-compose upは、立ち上げるサービスを明示的に指定することができるからだ。

$ docker-compose up db redis worker

これでapp以外のサービス全てが起動する。

次に、Railsをローカルで動かす。

$ bundle install --path vendor/bundle
# install dependencies...

$ bundle exec rails s

これで、起動は完了する。あくまで起動のみ。

まだ問題が残っている。

ローカルのRailsから、コンテナのDBやRedisにアクセスする

前述の通り、docker-compose.ymlに記述されているdepends_onにより、記述するだけで対象のサービスをサービス名、ここではそれぞれapp,worker,db,redisというホスト名で解決できるようになっている。

さらに、DBやRedisのコンテナのポートをローカルにバインドすることを意識せずとも、db:5432といった形でappやworkerからアクセスすることができるようになっている。

ただし、これはあくまで全てのサービスがdocker-composeコマンド経由で立ち上がっている場合に限る。

ローカルでRailsを動かしている場合は、各コンテナがEXPOSEしているポートを明示的にローカルにバインドしてやる必要がある。

しかし、単純にdocker-compose.ymlを書き換えれば済む話ではないケースもある。

余談だけど

PostgreSQL 9.2のイメージが5432をEXPOSEしていることは以下からわかる。

github.com

(というかPostgreSQLはデフォで5432を使う。)

www.postgresql.jp

サーバとクライアントのデフォルトのポート番号をNUMBERに設定します。 デフォルトは5432です。

docker-compose.override.yml

開発をする際、docker-compose.ymlで複数のサービスを立ち上げることを前提としたWebアプリケーションでは、docker-compose.ymlがバージョン管理されていることが多いと思う。

例えばRailsをローカルで動かしたいモチベーションの1つである「Docker for Macが遅い」はMacユーザー特有の問題なので、 Mac以外を使っている開発者にRailsをローカルで動かすことを強制することはなるべく避けたい。

こんな時に役立つのが、docker-compose.override.ymlだ。

docs.docker.jp

簡単にいうと、docker-compose.ymlの一部または全部を上書きすることができる。

これを使うことで、バージョン管理されているdocker-compose.ymlを書き換えることなく、簡単に設定を変更することができる。

// docker-compose.override.yml
version: '2'
services:
  db:
    ports:
      - "5432:5432"
  redis:
    ports:
      - "6379:6379"

これで、ローカルの54326379がそれぞれ各コンテナでEXPOSEされているポートにバインドされる。

ちなみに、docker-compose.ymlに書かれている特定の設定を消すだけの場合、以下のように[]を渡してやれば良い。

version: '2'
services:
  app:
    ports: []

コンテナのworkerから、ローカルのRailsにアクセスする

ローカルのRailsからDBとRedisにアクセスできてめでたしめでたし、ではない。まだ問題が残っている。

workerからappへのリクエストができなくなってしまうのだ。

今までworkerはappというホスト名にリクエストを送れていたが、Railsがローカルで動くように変えてしまったため、Dockerホストのlocalhost:3000(あるいは127.0.0.1)に対してリクエストを送る必要がある。

しかし、単純にworkerからlocalhostというホスト名を使ってリクエストを送っても、問題は解決しない。

理由は簡単で、workerにおけるlocalhostとは、worker自身を指すからだ。

こんな時は、Dockerが用意している特別なホスト名を使えば良い。

host.docker.internal

このホスト名は、Dockerコンテナから見たときのDockerホストを指している。

docs.docker.com

なので、host.docker.internal:3000とすれば、コンテナのworkerからローカルのRailsにアクセスすることができる。

docker-compose.override.ymlを以下のように変更してあげて、

// docker-compose.override.yml
version: '2'
services:
  db:
    ports:
      - "5432:5432"
  redis:
    ports:
      - "6379:6379"
  worker:
    environments:
      - APP_HOST: 'host.docker.internal'

workerからリクエストを送る時はこんな感じの実装にしてやる。

require 'http'

const host = process.env.APP_HOST || 'app'
const options = {
  host: host,
  # ...
}

const request = http.request(options,  res => { ... })
# ...

optionsの詳細についてはNode.jsのドキュメントを参照すると良い。

nodejs.org

これで、ローカルでRailsを動かしつつ、残りのサービスをうまくコンテナで起動できるようになった。

おわりに

ここまで書いておいてアレなんだけど、Dockerの良さである環境の差異を取り除くメリットが失われるので万人にお勧めできるような手法ではないかも。

Dockerが遅いからMac辞めたみたいな話をたまに見るんだけど、マウントの遅さについては割と生産性に直接関わってくるので無視できないことがだんだん理解できるようになってきた。

joker1007.hatenablog.com

とはいえこれだけで自分がMac辞める動機にはならないと思う、今のところ...。