Dockerコンテナ内で動かしている Laravel Mix HMRのホスト名を0.0.0.0にするのは甘えなので

おはようございます chatii です。

Laravel Mix 捨ててますか?いいですね、羨ましいです。

Docker の話です

TL;DR

ループバックアドレスエイリアスと '*.test' ホスト名を使ってDocker運用しようとしたらLaravel MixのHMRにうまく繋がらなかった

試行錯誤したら↓の設定でいけた

mix
.webpackConfig({
devServer: {
// webpack-dev-server が待ち受けるホスト
host: '0.0.0.0',
inline: true,
overlay: true,
disableHostCheck: true,
},
})
.options({
hmrOptions: {
// /hot に書き込まれる(ブラウザが参照する)ホスト&ポート
host: 'hoge.chatii.test',
port: 8080,
},
})

 

 

本文

でぃーほりさんの下記記事 (or Qiita) を読んで、よっしゃいろんな案件で使えるようにDockerを整理しよ!ってなった時に Laravel Mix で躓いた話です

wand-ta.hatenablog.com

この 127.x.y.z を使ってどう開発するかのルール決めをしましたが、それについてはまた記事にしたいと思います。受託だとイイカンジになると思うルール。

 

本題。

次のような構成でDockerが組まれています。

127.10.0.10 は lo0 に付与したIPです。このIPに対して `hoge.chatii.test` みたいなホスト名を /etc/hosts に記述しています

nginx (127.10.0.10:80)

php-fpm

postgresql (127.10.0.10:5432)

workspace (127.10.0.10:8080)

よくある構成です。この上で Laravel を動かしています。

artisan や npm などは workspace 内で動かします。 

動作確認して特に問題なく、上記ホスト名でアプリケーションの動作はできました。

 

しかし、そう、Laravel Mixではまりました。

 

具体的には Laravel Mix で HMR を使った場合です。

Docker内でHMRを有効にした Laravel Mix を使う際、よくある対応としては webpack.mix.js で設定する中で

.options({
hmrOptions: {
host:
'0.0.0.0',
port: 8080,
},
})

として、webpack-dev-server がどのアクセス元からでも繋がるようにする、というのがありますが…

これだと、今回のローカルループバックアドレスを複数使った運用に適しません。というか破綻します。

 

この運用のキモとして、「ポート番号の取り回しを気にしなくて良い」というのがありますが、上記のような host の設定だと、別の案件でもLaravel MixでHMRを8080ポートで使うときに競合します。

 

ブラウザがリクエストするのは、http://0.0.0.0:8080/js/app.js のようになりますが、別案件でも同じアドレスにJS/CSSをリクエストしてしまいます

(面倒なので検証してませんが先に立ち上げた webpack-dev-server のほうに繋がると思います)

 

また、上記 0.0.0.0 にする対応をする場合、webpack-dev-server へのオプションに

.webpackConfig({
devServer: {
disableHostCheck: true,
},
})

これを設定しますね。Webサーバーとwebpack-dev-serverのホスト名が一致しているか確認する機能を無効にしています。

 

しかし今回、「ホスト名は統一できる」はずです。

Webサーバーは `hoge.chatii.test:80` で、webpack-dev-server は `hoge.chatii.test:8080` にできるはずです。

`0.0.0.0:8080` でHMRが動作することを確認して…ここから試行錯誤が始まりました。

 

まずは素直に host に ホスト名を設定してみます

.options({
hmrOptions: {
host:
'hoge.chatii.test',
port: 8080,
},
})

結果。`No Content` ではなく、レスポンスなしになりました。下はホストマシンからCurlを叩いた様子。

curl hoge.chatii.test:8080
curl: (52) Empty reply from server

なお workspace コンテナ内で自身に向かって叩くとレスポンスあり。

つまり、webpack-dev-server は動いているものの、ホストからアクセスできない状態です

ブラウザで開いた際、JSへのリクエストは `http://hoge.chatii.test:8080/js/app.js` となっているので、mix() ヘルパの動きは正しそうです

 

`host: '0.0.0.0'` の指定のように、リクエストを受け付ける設定が必要になりそうなのがわかります。

 

ここで、webpack-dev-server のドキュメントを読んで、使えそうな設定を探ってみました。

webpack.js.org

webpack-dev-server にも `host` というオプションがありました。

ダメ元でここに設定してみます。

.webpackConfig({
devServer: {
host: 'hoge.chatii.test',
inline: true,
overlay: true,
disableHostCheck: true,
},
})
.
options({
hmrOptions: {
// /hot に書き込まれる(ブラウザが参照する)ホスト&ポート
host:'hoge.chatii.test',
port: 8080,
},
})

現象は変わらず、レスポンスなしです。

 

ここで、Laravel Mixがどんな余計なお世話なことをしているか確認します。

Laravel Mix は webpack-dev-server を起動するためのJSと、mixヘルパーのPHPとわかれています。

まずはmixヘルパーを掘っていくと

if (file_exists(public_path($manifestDirectory.'/hot'))) {
$url = rtrim(file_get_contents(public_path($manifestDirectory.'/hot')));

if (Str::startsWith($url, ['http://', 'https://'])) {
return new HtmlString(Str::after($url, ':').$path);
}

return new HtmlString("//localhost:8080{$path}");
}

こんなふうに、'/public/hot' を読み取ってます。これはただのテキストファイルで、HMRを有効にして Laravel Mix を起動すると、URLが書かれて出力されます。

mixヘルパーがHMRの時にはLaravel Mixのほうを読み込むのはこれのおかげですね

さて、では `hot` を出力しているのはどこでしょう?

`laravel-mix/src/index.js` にありました

Mix.listen('init', () => {
if (Mix.shouldHotReload()) {
let http = process.argv.includes('--https') ? 'https' : 'http';
let port = process.argv.includes('--port')
? process.argv[process.argv.indexOf('--port') + 1]
: Config.hmrOptions.port;

new File(path.join(Config.publicPath, 'hot')).write(
http + '://' + Config.hmrOptions.host + ':' + port + '/'
);
}
});

これを見ると、`Config.hmrOptions.host` を使ってます

つまり、`hmrOptions.host` には `hoge.chatii.test` を設定する、で合っているということです。

また、Laravel Mix用の設定 `hmrOptions` は内部でwebpack-dev-serverの設定 `webpackConfig` とマージされ、webpack-dev-server の host として使われるのは、`webpackConfig.devServer.host` が優先されます。

ということは、`webpackConfig.devServer.host` に '0.0.0.0' を書くことでコンテナ外からリクエストを受け付けられるようになるはず

つまり

mix
.webpackConfig({
devServer: {
// webpack-dev-server が待ち受けるホスト
host: '0.0.0.0',
disableHostCheck: true,
},
})
.options({
hmrOptions: {
// /hot に書き込まれる(ブラウザが参照する)ホスト&ポート
host:'hoge.chatii.net',
port: 8080,
},
})

こういう設定になります

 

これで、`hot` には `http://hoge.chatii.net:8080` が書き込まれ、mixヘルパは「ホスト名を指定されたリソースURL」を出力し、webpack-dev-server は workspaceコンテナ外からリクエストを受け付けられる、と、目的を達することができました

別案件が同時にHMR立ち上がっても大丈夫!

わからないこと

`docker ps` したときにポートバインディングが見られますが

docker-compose.yml には

ports:
- "127.10.0.10:80:80"

 と設定しているのに

36f0cdd890b5 hogehoge_nginx "/bin/sh -c ' /bin/b…" 14 hours ago Up 14 hours 127.0.0.1:32800->80/tcp hogehoge_nginx_1 

`127.0.0.1:32800->80/tcp` こうなるのはなぜ…?Macだから…?