Nuxt3のドキュメントに従って生成しただけのプロジェクトに、$ npm run devで起動したNuxtのアプリケーションにブラウザでアクセスすると、そのブラウザが再読込を繰り返すという症状に遭遇しました。

原因は、私がWebSocketで通信できるようにポートを設定していないことでした。この問題にNuxtやDocker、Docker-Composeなどは直接の関係がありませんでした。ただし、Dockerを使わずにホストで直接Node.jsを動かしていたら、この問題には遭遇しなかったはずです。

本問題発生時の構成

ホストでLXCが起動しています。そのLXCの中でDocker-Composeが起動しています。そのDocker-ComposeによりNode.jsコンテナが起動しています。その中でNuxtが起動しています。ブラウザはホストで起動しています。

ホストでLXCが起動しています。そのLXCの中でDocker-Composeが起動しています。そのDocker-ComposeによりNode.jsコンテナが起動しています。その中でNuxtが起動しています。ブラウザはホストで起動しています。

Docker-Composeは、Docker-Composeのホスト(つまりLXC)の3000番ポートとNode.jsコンテナの3000番ポートをマッピングしています。ホストで起動しているブラウザから、LXCのIPアドレスを指定することで、Nuxtへアクセスします。

内容を大幅に省略ていますが、docker-compose.yamlの内容です。

version: "3.8"

services:
  mycontainer:
    ports:
      - 3000:3000

解決までの道のり

NuxtをProductionモードで実行する

問題発生時、NuxtをDevelopmentモードで起動していました。

$ npm run dev

代わりにProductionモードで実行します。Nuxt3はProductionモードのときには、環境変数NUXT_HOSTを与えないと外部からのアクセスを受け付けてくれません。

$ npm run build
$ NUXT_HOST=0.0.0.0 npm run start

Productionモードで起動すると、本問題が発生しないことが確認できました。

Developmentモードで発生するが、Productionモードで発生しないことから、HMR(Hot Module Replacing)が原因ではないかと疑いました。Developmentモードを使用したいので、ここからは再びDevelopmentモードで原因の解決を進めました。

HMRを無効化しても再読込される

HMRが原因と疑ったので、早速これを無効化することを考えました。nuxt.config.tsでHMRを無効化を指定できます。

export default defineNuxtConfig({
  vite: {
    server: {
      hmr: false
    }
  }
})

残念ながら、これは解決に繋がりませんでした。

LXCの外に出る

HMRを疑いつつも確信がないので、他も疑ってかかります。

ホストで直接Podmanを動かしました。PodmanはDockerと互換性のあるコンテナエンジンです。標準でルートレスで使える点が気に入っています。

これを使い、LXCを省きました。

$ podman run --rm -it -p 3000:3000 node bash
$ apt update
$ apt install -y git
$ npx nuxi init nuxt3-app
$ cd nuxt3-app
$ npm install
$ npm run dev

ブラウザからhttp://localhost:3000とアクセスします。残念ながら、これでも問題の解決には至りませんでした。

本記事の冒頭でWebSocketのためにポートを設定していないことが原因だったと書いていることから、察しの良い人は気がつくかもしれません。もしこここでhttp://{コンテナのIPアドレス}:3000とアクセスしていたら、再読込が繰り返される症状は発生しなかったはずです。

ブラウザのコンソールを永続化する

私が使っているブラウザはFirefoxです。

ブラウザのコンソールに有益な情報が何も出ていないと思っていたのですが、ログの永続化をしたところ以下のログが表示されていることが分かりました。

Firefox can’t establish a connection to the server at ws://{LXCのIPアドレス}:24678/_nuxt/.
[vite] server connection lost. polling for restart...

polling for restart...といういかにもなログです。このログを出力している箇所をは以下の内容でした。

// ping server
socket.addEventListener('close', async ({ wasClean }) => {
    if (wasClean)
        return;
    console.log(`[vite] server connection lost. polling for restart...`);
    await waitForSuccessfulPing();
    location.reload();
});

socketのcloseイベントから実行される関数内にlocation.reload();があります。ここが評価されればブラウザは再読込します。

さらにsocketの定義を探しました。

const socketProtocol = null || (location.protocol === 'https:' ? 'wss' : 'ws');
const socketHost = `${null || location.hostname}:${"24678/_nuxt/"}`;
const socket = new WebSocket(`${socketProtocol}://${socketHost}`, 'vite-hmr');

先にログで確認したFirefox can’t establish a connection to the server at ws://{LXCのIPアドレス}:24678/_nuxt/.を補強する内容です。24678番ポートを使ってWebSocketを使おうとしています。このWebSocketが閉じられると、ブラウザが再読込するよう書かれていますので、本症状に合致します。

Docker-ComposeにWebSocketのポートもマッピングさせて解決

docker-compose.yamlに24678番ポートについて追記し、Docker-Composeを再起動します。

version: "3.8"

services:
  mycontainer:
    ports:
      - 3000:3000
      - 24678:24678

これでViteでブラウザが再読込が繰り返す問題は解決しました。