webpack-dev-serverでDockerコンテナ上のアプリをホットリロードする

本番環境がDockerコンテナで運用されている場合、ローカルでもDockerファイルで開発環境を作りたい場合があると思います。
その場合、ローカルのファイルに変更を加えるたびにDockerコンテナを逐一ビルドし直すのはしんどいので、ホットリロードが欲しいところです。  

この記事では、Webpackを使用するフロントエンド 開発において、[webpack-dev-server](https://github.com/webpack/webpack-dev-server)をDockerコンテナ上で使用し、コンテナ環境のホットリロードを実現する方法について紹介します。  

## 前提
- Webpackを使用（サンプルコードはReactで書きましたが、Webpackを使っているならなんでも良いです。）
- 本番環境がDockerコンテナで運用されている or したい

## ローカル環境でホットリロードする
まず、普通にローカル環境でホットリロードする環境を作ります。
webapck-dev-serverをインストールしましょう。
```$ npm install --save-dev webpack-dev-server```

次に[Webpack公式](https://webpack.js.org/configuration/dev-server/)にしたがって```webpack.config.js```に
以下の設定を追加します。

```javascript:webpack.config.js
const path = require("path");
module.exports = {
  // 略
  devServer: {
    contentBase: path.join(__dirname, "dist"),
    hot: true,
    open: true,
    port: 9000,
  }
}
```

npm scriptsにwebpack-dev-serverを実行するコマンドを追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213222/68ab573a-bbab-a3fd-9ee6-5afaa307a5db.png)

```$ npm run dev```で実行すると、アプリケーションのビルドを行い、ブラウザを立ち上げて画面表示までしてくれます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213222/7232e5b2-c231-0541-cf8c-5ea180af4206.png)


ホットリロードされるか確認しましょう。ローカルのアプリのコードに修正を加えます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213222/159cd42b-2fc4-9d7e-0f5b-16789504712e.png)

保存したタイミングでブラウザを確認すると、ファイルに加えた修正がブラウザに反映されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213222/8038538f-03ab-6e53-67ce-39cdb3d34602.png)



ローカル環境でのホットリロードが動くことが確認できました。
次は、これをローカルのDockerコンテナ上で動くアプリケーションで実現します。  

## docker-composeでローカルにDockerコンテナを立ち上げる

下記のようなDockerファイルが既に運用されていたとします。（なかったら用意します。）

```dockerfile:Dockerfile
# ビルドステージ
FROM mhart/alpine-node:11 as build
COPY . /app
WORKDIR /app
RUN npm install
RUN npm run build

# 本番環境ステージ
FROM mhart/alpine-node:11
RUN npm install --global serve
WORKDIR /
COPY --from=build /dist .
CMD ["serve", "-p", "3000", "-s", "."]
```
コンテナサイズの最適化のため、マルチステージビルドしています。
ホットリロードするにはソースコードを監視しておく必要があるため、ソースコードが含まれるビルドステージをターゲットに指定し、そこでwebpack-dev-serverを起動します。

そのため、以下のdocker-composeファイルを作成します。

```docke-compose.yaml
version: "3.4"

services:
  web:
    build:
      context: .
      target: build
    volumes: 
      - ./src:/app/src
    ports:
      - 8081:9000
    command: npm run dev
```
  
（```target```プロパティは[バージョン3.4](https://matsuand.github.io/docs.docker.jp.onthefly/compose/compose-file/compose-versioning/#version-34)から指定可能です。 ） 


ここでvolume mountしているファイルは、ローカルとコンテナ内で同期されるので、ローカルのファイルが変更されるとコンテナ内のファイルも変更されます。
この時コンテナ内でwebpack-dev-serverが起動していると、コンテナ内でのファイル変更を検知し、差分ビルドが実行されます。  

最後に、webpack設定ファイルの```devServer```に以下の設定を追加します。  

```diff:webpack.config.js
const path = require("path");
module.exports = {
  // 略
  devServer: {
    contentBase: path.join(__dirname, "dist"),
+   host: 0.0.0.0
    hot: true,
    open: true,
    port: 9000,
  }
}
```  

ホストがデフォルト設定の```localhost```のままだと、コンテナ環境のlocalhostで待ち受けるため、ホストマシンからみたlocalhostでアクセスできません。
なので、到達可能にするためホストに```0.0.0.0```を指定します。

以上で設定が完了しました。
実際にコンテナを立ち上げ、ホットリロードの確認をしてみましょう。  

下記コマンドを実行し、コンテナを立ち上げます。
```$ docker-compose build```
```$ docker-compose up```

立ち上がったら、アプリにアクセスしてみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213222/50fc935a-24f6-56dc-a46d-f641ae3ce7db.png)



アクセスできたので、試しにローカルのファイルを修正します。 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213222/6b25effe-259b-80ac-0e94-f05f8ea064ac.png)

 

ブラウザをリロードすると、画面に変更が反映されていることが確認できました。 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213222/22a8469c-e69a-fecb-5512-0f368a950166.png)
 
コンテナ上だとローカルで動かした時のようなブラウザのオートリロードが効かなくなるのですが、これに関してはまだ解決できていません。。
が、ひとまずローカルのファイルを変更するだけで、ビルド結果が反映されるようになりました！:tada:

以下、今回の検証に使用したサンプルリポジトリになります。
https://github.com/yuta4j1/react-docker-hotreload

<br>

## （余談） docker-composeファイルの必要性
[ドキュメント](https://docs.docker.jp/engine/reference/builder.html#volume)にも記載がありますが、Dockerfileのボリュームマウントはホストマシンのローカルディレクトリのマウントをサポートしていません。
ローカル環境への依存はDockerの可搬性のポリシーに反する、ということらしいです。
> コンテナ実行時に宣言されるホストディレクトリ: ホストディレクトリ（マウントポイント）は、その性質からして、ホストに依存するものです。 これはイメージの可搬性を確保するためなので、設定されたホストディレクトリが、あらゆるホスト上にて利用可能になるかどうかの保証はありません。 このため、Dockerfile の内部からホストディレクトリをマウントすることはできません。 つまり VOLUME 命令は host-dir （ホストのディレクトリを指定する）パラメータをサポートしていません。 マウントポイントの指定は、コンテナを生成、実行するときに行う必要があります。

一方、docker-composeはローカルディレクトリのファイルマウントをサポートしているため、ローカルのファイルとコンテナ内のファイルの同期をとってホットリロードするには、docker-composeでの設定が必要ということになります。