2021-10-15

Webpackerを外したRailsをDockerで開発できるようにする

前回、無事にホットリロードまで使えるようになり、フロントエンド開発環境としてはほぼ問題ない状態になったと思うものの、Railsサーバーとwebpack-dev-serverのプロセスを2つ立ち上げないといけないのはとても面倒なので、今回はその部分をなんとかしていく

ひと昔前ならForeman gemとか使ってやっていたが、今時はdocker-composeを使って2つコンテナを立ち上げて対応すると思うのでその方向でやっていく
正直そこまでDockerの知見がないので、かなり良いのではないかと言われているEvilMartins流の構成を最大限参考にさせていただき、ついでにDockerの知見も得ていこうと思う
(リポジトリはこちら)

EvilMartins流の構成をとりあえずブッ込む

リポジトリが↓にあるので、細かいことはわからないけど、とりあえずぶっ込んで動くところまでやって、そのあと1つ1つ調べていく
https://github.com/evilmartians/terraforming-rails/tree/master/examples/dockerdev

今回docker化するサンプルアプリでは不要なものもあるので、そのあたりを除外した構成で入れていく
具体的には以下の箇所を削除

  • redisは使ってないので削除
  • sidekiqは使ってないので削除
  • bootsnapは使ってないので削除
  • webpackerではなく素のwebpackなのでそのあたりを調整
  • mimemagicを使ってないのでDockerfileから該当部分を削除
  • runnerコンテナは欲しい人がいれば復活させることにして削除

M1 Mac環境でyarnの導入に失敗する

今回M1 Macの環境で構築したんだけど、yarnをソースリストに入れる以下の箇所がエラーになった
https://github.com/evilmartians/terraforming-rails/blob/6d4a2c2297b2ebd686430ddbf107d80f0d006644/examples/dockerdev/.dockerdev/Dockerfile#L42-L44

RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
  && echo 'deb http://dl.yarnpkg.com/debian/ stable main' > /etc/apt/sources.list.d/yarn.list

ググると同じ症状の人がいたのでコードを拝借するが、どうやらwgetが入っていないようで再びエラーになったので
ここにwgetのインストールも含めてあげると無事にインストールできた

Railsとwebpack-dev-serverが連携できるようにする

動作環境がDocker化したことにより、webpack-dev-serverがlocalhostで待ち受けてもコンテナ内に他に誰もいないので正しく動作しなくなる
従って webpack.config.jsdevServerhost 設定で 0.0.0.0 を指定して、コンテナ外部からアクセスできるようにして、Railsと連携できるようにする

ということで無事にDocker Composeで動くようになった(ここまでの差分はこちら)
アプリルート超絶荒れがちなので .dockerdev にまとめるのいいと思った

EvilMartins流Docker化が何をやっているのか見ていく

無事動くようになったので、どんなことをやっているのか見ていく
まずはdocker-compose.ymlの内容をチェックして、そこから参照されているファイルを掘っていく

x-appブロック

まずx-appの部分を見ていく

x-app: &app
  build:
    context: .dockerdev
    dockerfile: Dockerfile
    args:
      RUBY_VERSION: '2.7.3'
      PG_MAJOR: '14'
      NODE_MAJOR: '14'
      YARN_VERSION: '1.22.15'
      BUNDLER_VERSION: '2.2.29'
  environment: &env
    NODE_ENV: ${NODE_ENV:-development}
    RAILS_ENV: ${RAILS_ENV:-development}
    YARN_CACHE_FOLDER: /app/node_modules/.yarn-cache
  image: remove_webpacker_sample:1.0.0
  tmpfs:
    - /tmp
  • &app

    • x-appの横にある &app はRailsでもお馴染みのやつで、定義を使いまわせるようにするためのもの
    • どこか必要な箇所で <<: *app と呼べば↑の定義をまるまる追加することができる
  • build

    • Docはここ
    • コンテナイメージのビルドの要件を定義する箇所
    • contextはDockerfileがあるディレクトリへのパス、またはgitリポジトリを指定するところ
    • dockerfileはDockerfileを別名で用意した場合に、その名前を指定するためのところ
      • 今回は普通にDockerfileという名前で使っているので指定しなくても良さそう
    • argsはDockerfileのARG値を設定するところ
      • Dockerfile内部で参照していることがわかる
      • コンテナ内部で参照できる環境変数とは別なので注意
  • environment

    • Docはここ
    • コンテナ内部で参照できる環境変数を定義するところ
    • 環境変数は env_file でも設定できるが environment でも設定があった場合はそちらが優先される
    • 見慣れない ${HOGE:-hoge} という構文はここに説明があるが、手元でも簡単に試して確認できる => echo ${HOGE:-hoge} HOGEの設定がなければ hoge と出力される
  • image

    • Docはここ
    • コンテナを起動するためのイメージを指定するところ
    • 指定されたイメージがローカルになければdocker hubとかからpullしてくるという動作をする
      • ただし、imagebuild が両方指定されている場合は build が優先して実行され、 image のpullは発生しない (ここではこの動き)
    • 今回 image: remove_webpacker_sample:1.0.0 という感じでimageを指定しているので
      $ docker images
      REPOSITORY                TAG       IMAGE ID       CREATED       SIZE
      remove_webpacker_sample   1.0.0     74b11c553c66   3 hours ago   573MB
      
      こんな感じにイメージが1つだけ作成されるが、もしimageを指定しなかった場合は
      $ docker images
      REPOSITORY                        TAG       IMAGE ID       CREATED       SIZE
      remove_webpacker_sample_webpack   latest    74b11c553c66   3 hours ago   573MB
      remove_webpacker_sample_rails     latest    74b11c553c66   3 hours ago   573MB
      
      といった感じで作成しようとしているコンテナの数(_webpack_rails)だけイメージが作成されてしまうようで、とてもリソース効率が悪い
      例えdocker hubなどで配布しないとしても、セマンティックバージョニングしないとしても、とりあえず指定だけはしておいた方が良さそう
      あと、ここで最悪と言われている latest が勝手に使われるのも本来良くないっぽい
  • tmpfs

    • こっちのDocは説明がしょぼいので、こっちを見ると良さそう
    • Dockerでは通常コンテナを破棄するとデータは消滅してしまうので、必要なデータに対しては volumebind mount を使ってホストマシンとコンテナでファイルを共有し、データを保持するようにするのが一般的
    • tmpfs は上記2つとは異なりホストのメモリ上に永続化され、コンテナが停止すると tmpfs は削除される
    • 要するに消えちゃうけど読み書きが高速なので、/tmp みたいなディレクトリをマウントするのにうってつけ
    • また、ホストにもコンテナにも残したくないような一時データにも最適なので、セキュリティ目的か今回のようにパフォーマンス目的で使うと良い

x-backendブロック

続いて x-backend ブロックも見ていく
関連しているので volumes も載せている

x-backend: &backend
  <<: *app
  stdin_open: true
  tty: true
  volumes:
    - .:/app:cached
    - rails_cache:/app/tmp/cache
    - bundle:/usr/local/bundle
    - node_modules:/app/node_modules
    - packs:/app/public/packs
    - .dockerdev/.psqlrc:/root/.psqlrc:ro
    - .dockerdev/.bashrc:/root/.bashrc:ro
  environment:
    <<: *env
    DATABASE_URL: postgres://postgres:postgres@postgres:5432
    WEBPACK_DEV_SERVER_HOST: webpack
    WEB_CONCURRENCY: 0
    HISTFILE: /app/log/.bash_history
    PSQL_HISTFILE: /app/log/.psql_history
    EDITOR: vi
  depends_on:
    postgres:
      condition: service_healthy

volumes:
  postgres:
  bundle:
  node_modules:
  rails_cache:
  packs:
  • <<: *app
    • ↑で説明した通り、x-appの定義を全てmixinしている
  • stdin_opentty
    • こちらの記事が超詳しいので、こちらから引用させていただくと
      • stdin_opendocker run コマンドの -i オプションに相当するもので、これをつけないと(trueにしないと)terminalからの入力を受け付けなくなる
      • ttydocker run コマンドの -t オプションに相当するもので、これをつけないと(trueにしないと)標準入出力がterminalに繋がらず、インタラクティブな操作ができない
  • volumes
    • Dockerコンテナ内部にデータを作成しても、コンテナを破棄するとデータも一緒に消えてしまうため、コンテナの外にボリュームと呼ばれるデータ領域を用意し、そこを利用することで永続化することができる
      • そのボリュームを指定するためのところが volumes
      • いくらコンテナを破棄しても、明示的に破棄されない限りvolumeの中のデータは残るということ
    • ボリュームは特定のコンテナ専用にも、複数のコンテナから参照できるようにもでき、ここではその両方が使われている
      • 例えば .:/app:cached これは、ホストのカレントディレクトリ(.)を、コンテナ専用のボリュームとして /app へマウントしている(公式で言うところのバインドマウントのこと)
      • 一方、 rails_cache:/app/tmp/cacherails_cache という名前付きのボリュームを、 コンテナ内の /app/tmp/cache へとマウントしている(公式で言うところの**(名前付き)ボリューム**のこと)
    • .:/app:cachedcachedここに説明がある通り、ホスト上のファイルの更新がコンテナ上に反映されるまで、多少の遅延が発生することを許可する代わりに、パフォーマンスを高めている
  • depends_on
    • Docはここ
    • コンテナ間の起動と停止の依存関係を支持するところ
    • ここでの場合「x-backendをmixinしたサービスは、コンテナ作成時にpostgresコンテナが先に作成される。また、コンテナが破棄される時はpostgresコンテナが先に破棄される」ように指定されている
      • 要するに常に先に作成・破棄されるようになる
    • 上記Docにも書かれているが、docker-composeではサービスが開始する前に、依存するサービスが開始されていることを待機することができる
    • ここではpostgresコンテナのヘルスチェックがOKとなったあと、x-backendがmixinされたコンテナが作成される
      • railsのコンテナはDBが先に動作していないとエラーになるのでこれが必要

servicesブロック

rails, postgres, webpackの3つのコンテナを作成しているので、1つ1つ見ていく
まずはrailsコンテナ

rails:
  <<: *backend
  command: bundle exec rails server -b 0.0.0.0
  ports:
    - '3000:3000'
  • 上述した x-backend の設定が全て含まれるようになっている
    • すでに説明したが image の指定はあるが、build も指定されているので、用意したDockerfileからコンテナが作成される
  • command はコンテナで実行するプロセスのコマンドで、Railsサーバーを起動し、外部からのアクセスを許可している(0.0.0.0 はその機器にある全てのIPアドレスで待ち受けるという意味)
  • ports はコンテナの3000番ポートをホストの3000番ポートにマッピングしている(Railsは3000番ポートを使うのでこの指定が必要)
    • ちなみに ホストのポート:コンテナのポート となる

続いてpostgresコンテナ

postgres:
  image: postgres:14.0
  volumes:
    - .dockerdev/.psqlrc:/root/.psqlrc:ro
    - postgres:/var/lib/postgresql/data
    - ./log:/root/log:cached
  environment:
    PSQL_HISTFILE: /root/log/.psql_history
    POSTGRES_PASSWORD: postgres
  ports:
    - 5432
  healthcheck:
    test: pg_isready -U postgres -h 127.0.0.1
    interval: 5s
  • こいつは x-backendapp をmixinしていないので、明示的に image が指定されている
  • volume は↑で説明した通りだが、それぞれ何なのか見ていく
    • .psqlrc はpostgresqlを対話的に実行することができる psql コマンドを、ここに設定を書くことでより便利にしてくれるファイル。bashrc みたいなものだと考えればよい(中までは深く見ない)
    • 名前付きのボリューム postgres とマウントしている /var/lib/postgresql/dataここで指定されている物理的なデータの保存場所(PGDATA で明示的に指定することができる。初期値/var/lib/pgsql/data)
    • 最後のはログディレクトリをマウントしている(PSQL_HISTFILE/root/log 以下に保存するようにしている)
  • ports は単体で 5432 と書かれているが、これは「コンテナの5432番ポートを公開し、ホストのポートは使える番号をDockerが適当に選択する」という挙動になる
  • healthcheck は↑のx-backendブロックのところでちょっとだけ触れたが、このpostgresコンテナが「健全」であるかどうかを判断するために使われる
    • test はコンテナの健全性をチェックするために使われるコマンドで、pg_isreadyコマンドで接続確認をしている
    • interval はそのまんま5秒毎に実行される

たまに起動が待てなくて云々という話が出るけど、こうやってヘルスチェックやって待機してあげればいいのか〜
これは勉強になった

最後にwebpackコンテナ
といってもここは僕が独自に書き換えているので、EvilMartins流ではない

webpack:
  <<: *app
  command: yarn dev-server
  ports:
    - '8080:8080'
  volumes:
    - .:/app:cached
    - bundle:/usr/local/bundle
    - node_modules:/app/node_modules
    - packs:/app/public/packs
  environment:
    <<: *env
  • x-backend ではなく app の設定を使っているのはそのまま
  • command はWebpaker使っていないので package.json に定義したnpm scriptを指定
  • ports もwebpack-dev-serverデフォルトのポートをマッピングしている
  • volumes もgemとかnpmパッケージとかビルドファイルをrailsコンテナと使いまわせるようにしているだけ

終わりに

ということで、不要と思われる箇所を削除したコミットはこちら
これで(多分)必要なところだけを残したymlになったと思われる

参考記事