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していることは以下からわかる。
(というかPostgreSQLはデフォで5432
を使う。)
サーバとクライアントのデフォルトのポート番号を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
だ。
簡単にいうと、docker-compose.yml
の一部または全部を上書きすることができる。
これを使うことで、バージョン管理されているdocker-compose.yml
を書き換えることなく、簡単に設定を変更することができる。
// docker-compose.override.yml version: '2' services: db: ports: - "5432:5432" redis: ports: - "6379:6379"
これで、ローカルの5432
と6379
がそれぞれ各コンテナで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ホストを指している。
なので、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のドキュメントを参照すると良い。
これで、ローカルでRailsを動かしつつ、残りのサービスをうまくコンテナで起動できるようになった。
おわりに
ここまで書いておいてアレなんだけど、Dockerの良さである環境の差異を取り除くメリットが失われるので万人にお勧めできるような手法ではないかも。
Dockerが遅いからMac辞めたみたいな話をたまに見るんだけど、マウントの遅さについては割と生産性に直接関わってくるので無視できないことがだんだん理解できるようになってきた。
とはいえこれだけで自分がMac辞める動機にはならないと思う、今のところ...。