💎

[nginx] Flutter on the Web with Ruby on Rails

2023/01/17に公開

忙しい方向け

/rails 以下だけを Ruby on Rails アプリケーションにルーティングして、その他を Flutter on the Web アプリケーションにルーティングしたい場合は Ruby on Rails の assets ディレクトリを Flutter と競合しないように別の場所に逃して

config.assets.prefix = '/shared/assets'

nginx 設定をこんな風にして

upstream rails {
  server unix:///var/www/apps/rails/tmp/run/rails.sock;
}

server {
  listen 80 default_server;
  listen [::]:80 default_server;

  root /var/www/apps/flutter/web;
  index index.html index.htm;

  server_name example.com;

  location / {
    try_files $uri $uri/ /index.html;
  }

  location ~ ^/(shared/assets|rails) {
    root /var/www/apps/rails/public;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_redirect off;

    proxy_set_header X-Forwarded-Server $host;
    proxy_set_header X-Forwarded-Host   $host;
    proxy_set_header X-Forwarded-PROTO  $scheme;
    proxy_pass http://rails;
  }
}

Ruby on Rails の assets のキャッシュを消してからプリコンパイルしてアプリケーションを再起動して

$ bundle exec rails assets:precompile
$ bundle exec rails tmp:cache:clear
$ bundle exec pumactl stop
$ RAILS_SERVE_STATIC_FILES=true RAILS_ENV=production bundle exec puma -w 4 -d

nginx をリロードすれば同時に動かせます。

$ sudo nginx -s reload

前置き

要件が複雑すぎて記事タイトルで説明ができませんでした。これは Ruby on Rails アプリケーションと nginx で動かしている Cent OS 上のシステムを、Flutter on the Web アプリケーションで replace するんだけど一部サブシステムだけは同一サーバー内で Ruby on Rails アプリケーションのまま残したいあなたのための記事です。よくあるよね、そういうこと。

具体的には、仮にサービスのドメインが example.com だとした場合、example.com にアクセスされた場合は Flutter on the Web アプリケーションを表示して、example.com/rails 以下のサブディレクトリにアクセスされた場合のみ Ruby on Rails アプリケーションを表示する、といった要件です。実際に私が直面したリアルな要件は、フロントだけ Flutter on the Web に置き換えるんだけれど、管理画面的な機能だけは Ruby on Rails のサブシステムをそのまま残すというものです。

調べた感じ、こういったことは多分誰もやってなさそうですし、これからも誰もやらなさそうですが、結構面倒な思いをしたので架空のあなたに向けて記事として残します。

nginx の設定

Flutter on the Web を動かす

まず、Flutter on the Web を Firebase Hosting や GitHub Pages を使わずに自前のサーバーマシンで nginx で動かそうとしている人間がこの世に数えられるほどしかいなさそうでした。結論から言うと Flutter on the Web を動かす場合は以下のような設定で動きます。flutter build web でできたディレクトリが /var/www/apps/flutter/web に存在すると仮定します。

server {
  listen 80 default_server;
  listen [::]:80 default_server;

  root /var/www/apps/flutter/web;
  index index.html index.htm;

  server_name example.com;

  location / {
    try_files $uri $uri/ /index.html;
  }
}

誰かの設定をコピペして済ませようとググったら try_files $uri $uri =404; している記事が見つかりますが、これだとトップページ以外に直接 URL を叩いてアクセスされた時に 404 になってしまいます。どんな URL にアクセスされても必ず index.html を表示する必要があるので、 try_files $uri $uri/ /index.html; が正しいです。 404 は Flutter がやってくれます。

Ruby on Rails を動かす

次に仮に /rails にアクセスされた時だけは Flutter on the Web アプリケーションではなく Ruby on Rails アプリケーションの /rails にルーティングします。Ruby on Rails アプリケーションは /var/www/apps/rails にあり、puma で動いていると仮定します。

  server {
    listen 80 default_server;
    listen [::]:80 default_server;

    root /var/www/apps/flutter/web;
    index index.html index.htm;

    server_name example.com;

    location / {
      try_files $uri $uri/ /index.html;
    }

+  location ~ ^/rails {
+    root /var/www/apps/rails/public;
+    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+    proxy_set_header Host $http_host;
+    proxy_redirect off;
+
+    proxy_set_header X-Forwarded-Server $host;
+    proxy_set_header X-Forwarded-Host   $host;
+    proxy_set_header X-Forwarded-PROTO  $scheme;
+    proxy_pass http://rails;
+  }
  }

これで /rails にアクセスすると Ruby on Rails アプリケーションが動くようになりますが、CSS や画像ファイル等が読み込まれません。それらのファイルはデフォルトで /assets に入っているからですね。なので、/assets も Ruby on Rails にルーティングしてやることにします。

-  location ~ ^/rails {
+  location ~ ^/(assets|rails) {

すると Ruby on Rails アプリケーションで CSS や画像を(ブラウザーやサーバーのキャッシュが悪さをしていなければ)読み込めるようになると思います。ただ、すると今度は Flutter on the Web アプリケーションの画像ファイルが読み込まれなくなります。Flutter on the Web も画像ファイルを /assets に作るためです。

Ruby on Rails から assets を逃がす

Ruby on Rails も Flutter on the Web も /assets を使いたいと主張しますが、どちらかが譲らなければなりません。先に動いていたのは Ruby on Rails アプリケーションなので Flutter 側に譲ってほしいところですが、恐らく Flutter on the Web が /assets 以外のディレクトリに変更することは出来なさそうです。Flutter JP の slack や、Flutter コミュニティの slack や discord にお邪魔して聞いて回ったのですが、誰も答えを持っていなさそうでした。どこのコミュニティでも私の質問だけがスルーされて他の質問だけにレスが付く悲しい時間が流れ続けたので、私は諦めて Ruby on Rails アプリケーションに /assets を譲ってもらうことにしました。

config.assets.prefix

config/application.rb で config.assets.prefix にセットするだけでディレクトリが変わります。デフォルトは '/assets' です。これは分かってほしいんですが、譲り先のディレクトリの名前がマジで思い浮かばなかったので /shared/assets にしました。ダサい。ダサいがマジで思い浮かばなかったんだ勘弁してくれ。

config.assets.prefix = '/shared/assets'

サーバーのキャッシュクリア

これで assets プリコンパイルすると CSS や画像は読み込まれるのですが、Font Awesome や一部のサードパーティ経由のリソースがまだ /assets を参照し続ける問題が発生しました。これは結論から言うとサーバー側のキャッシュをクリアすることで解決できました。

$ bundle exec rails tmp:cache:clear

puma の再起動

bundle exec pumactl restart ではなく、ちゃんとサーバーを落としてから起動してあげる必要があります。

$ bundle exec pumactl stop
$ RAILS_SERVE_STATIC_FILES=true RAILS_ENV=production bundle exec puma -w 4 -d

nginx 設定の変更

/shared/assets を Ruby on Rails にルーティングします。

-  location ~ ^/rails {
+  location ~ ^/(shared/assets|rails) {

作業まとめ

最後に、サーバー側に Flutter on the Web アプリケーションのビルド済みディレクトリと Ruby on Rails アプリケーションのソースコード更新が終わったものとして、その後に行う本番環境作業手順をまとめておきます。

1. nginx 設定の更新

設定例です。

upstream rails {
  server unix:///var/www/apps/rails/tmp/run/rails.sock;
}

server {
  listen 80 default_server;
  listen [::]:80 default_server;

  root /var/www/apps/flutter/web;
  index index.html index.htm;

  server_name example.com;

  location / {
    try_files $uri $uri/ /index.html;
  }

  location ~ ^/(shared/assets|rails) {
    root /var/www/apps/rails/public;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_redirect off;

    proxy_set_header X-Forwarded-Server $host;
    proxy_set_header X-Forwarded-Host   $host;
    proxy_set_header X-Forwarded-PROTO  $scheme;
    proxy_pass http://rails;
  }
}

2. Ruby on Rails アプリケーションのキャッシュクリア

$ bundle exec rails tmp:cache:clear

3. Ruby on Rails アプリケーションの assets プリコンパイル

$ bundle exec rails assets:precompile

4. Ruby on Rails アプリケーションの再起動

$ bundle exec pumactl stop
$ RAILS_SERVE_STATIC_FILES=true RAILS_ENV=production bundle exec puma -w 4 -d

5. nginx の再起動

$ sudo nginx -s reload

以上。

Discussion