【Vite + React + Docker】ホットリロード付きのローカル環境構築

## 【初めに】
こんにちは。
@hayatohanaoka です。

休日に遊び感覚で `React` を `Docker` で動かせるようにしてみたところ、
ホットリロード周りでいくつか学びがあったので残します。
出来としては雑でお粗末なものかもしれないのですが、誰かの助けになれば幸いです。

## 【リポジトリ】
:mega:  「とりあえず中身見せろや！！」 
って人向けに、冒頭で置いておきます...
[sample-react](https://github.com/hayatohanaoka/sample-react)

## 【環境】
マシン
`M3 Mac`

コンテナマネージャ
`Rancher Desktop`

言語
`typescript 5.6.2`

ランタイム
`node 23.7.0`

ライブラリ
`react 18.3.1`

## 【ReactAppの作成】
[公式Docs](https://vite.dev/guide/) に従ってコマンド実行しただけです。
ここはそこまで大変でないのと、次のトピックが主題なので割愛。

## 【コンテナ化】
`Dockerfile` を作成して、コンテナ化していきます。
`Dockerfile` の解説を少し挟むと、コンテナの元になる `image` の作成の際に必要となるファイルです。
ここに書いたステップを元に `image` を作成し、保存します。
``` Dockerfile
FROM node:23-alpine
WORKDIR /app
# 依存関係のインストール
COPY package*.json ./
RUN npm install
# ソースコードのコピー
COPY . .
# アプリケーションの起動
CMD ["npm", "run", "dev"]
```
:::note warn
ローカルで実行するために簡単に用意したため、
ビルドと実行のステップを分けること（マルチステージビルド）はしていません。
:::
やっていることは非常にシンプルなので、初心者でも理解しやすいです。
1. 土台となるイメージを用意する（ `node:23-alpine` ）
2. `package.json`を元に、依存関係をインストールする
3. ソースコードを元に `npm run dev` でアプリケーションを起動する

こちらについても、特筆すべき点はありません。


## 【ホットリロードの有効化】
今回の記事の主題です。
編集が必要になったのは以下のファイルです。
1. docker-compose.yaml
1. vite.config.ts

上から順に解説します。

### `docker-compose.yaml`
複数のコンテナをどのように立ち上げるか設定できるファイル。
1. `image` の作成の際にどのDockerfileやファイルを参照するか
1. どのポートを繋ぎ合わせるのか
1. 環境変数には何を使用するか

等が設定できます。
```docker-compose.yaml
services:
  react:
    build:
      context: ../../
      dockerfile: environments/local/Dockerfile
    ports:
      - 3000:5173 # host_port:container_port
    volumes: # ホストのディレクトリをマウントして、コンテナ内のディレクトリに反映
      - ../../src:/app/src
    environment:
      # ホットリロードのために必要なオプション値
      - CHOKIDAR_USEPOLLING=true
      - WATCHPACK_POLLING=true
```
ここでは以下の工夫が必要でした。
1. ポートフォワード
    1. `localhost:3000` でリクエストしたかったので、 `ports` で指定
1. ボリュームマウント
    1. ここがないと、ローカルの変更がコンテ内部に反映されません
1. 環境変数の設定
    1. ここがないと、ソースコードの変更が検知されません
    1. `vite.config.ts` で設定することも可能
        1. ローカルで `npm run dev` を行う際に不要な設定のため、こちらで設定した

### `vite.config.ts`
```vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vite.dev/config/
export default defineConfig({
  plugins: [react()],
  // サーバがリッスンするホストとポートを設定
  server: {
    host: true,
    port: 5173,
  },
})
```
こちらでは以下の設定を行なっています。
1. viteが立ち上げるサーバーと通信可能なホストとポートを設定する
    1. `host`
        1. `0.0.0.0` または `true` を設定すると、全ホストを許可
        1. コンテナ内部の `localhost` と、 ホストマシンの`localhost` が別のものなので、
        繋ぎ合わせるために必要
    1. `port`
        1. 今回は `5173` を許可

図にするとこんな感じだと思います。

![スクリーンショット 2025-06-12 3.20.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2449619/18cb3607-c539-4f7c-af33-0ebe2dcda7e9.png)

# 【実行】
いよいよ実行です...！！
以下のコマンドを実行します。
```
$ cd environments/local 
$ docker compose up --build

Compose can now delegate builds to bake for better performance.
 To do so, set COMPOSE_BAKE=true.
[+] Building 2.4s (11/11) FINISHED                                                                                                                                                             docker:rancher-desktop
 => [react internal] load build definition from Dockerfile                                                                                                                                                       0.0s
 => => transferring dockerfile: 250B                                                                                                                                                                             0.0s
 => [react internal] load metadata for docker.io/library/node:23-alpine                                                                                                                                          1.4s
 => [react internal] load .dockerignore                                                                                                                                                                          0.0s
 => => transferring context: 2B                                                                                                                                                                                  0.0s
 => [react 1/5] FROM docker.io/library/node:23-alpine@sha256:a34e14ef1df25b58258956049ab5a71ea7f0d498e41d0b514f4b8de09af09456                                                                                    0.0s
 => [react internal] load build context                                                                                                                                                                          0.2s
 => => transferring context: 453.30kB                                                                                                                                                                            0.2s
 => CACHED [react 2/5] WORKDIR /app                                                                                                                                                                              0.0s
 => CACHED [react 3/5] COPY package*.json ./                                                                                                                                                                     0.0s
 => CACHED [react 4/5] RUN npm install                                                                                                                                                                           0.0s
 => [react 5/5] COPY . .                                                                                                                                                                                         0.5s
 => [react] exporting to image                                                                                                                                                                                   0.2s
 => => exporting layers                                                                                                                                                                                          0.2s
 => => writing image sha256:9ebfd4463af40681bafe16f2e4676938b1e58cfdd8323be570d88a39a672616e                                                                                                                     0.0s
 => => naming to docker.io/library/local-react                                                                                                                                                                   0.0s
 => [react] resolving provenance for metadata file                                                                                                                                                               0.0s
[+] Running 2/2
 ✔ react                    Built                                                                                                                                                                                0.0s 
 ✔ Container local-react-1  Recreated                                                                                                                                                                            0.0s 
Attaching to react-1
react-1  | 
react-1  | > sample-react@0.0.0 dev
react-1  | > vite
react-1  | 
react-1  | 
react-1  |   VITE v5.4.19  ready in 89 ms
react-1  | 
react-1  |   ➜  Local:   http://localhost:5173/
react-1  |   ➜  Network: http://172.21.0.2:5173/
```
うまくいってそう！！一応確認する。
```
$ docker ps
CONTAINER ID   IMAGE       COMMAND                CREATED         STATUS         PORTS                                         NAMES
0489acc9d69f   local-react "docker-entrypoint.s…" 2 minutes ago   Up 2 minutes   0.0.0.0:3000->5173/tcp, [::]:3000->5173/tcp   local-react-1
```

これで http://localhost:3000/ にもリクエストしてみると...

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2449619/192a31a2-0971-4c13-8982-450540d82032.png)

:ok: :ok: :ok: :ok: :ok: :ok: 

試しにsrc/main.tsを変更してみると、こんなログが！
```
react-1  | 6:30:55 PM [vite] page reload src/main.tsx
```

画面を確認。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2449619/98c799fb-3114-48e0-a745-cf7187a5ff13.png)

:ok: :ok: :ok: :ok: :ok: :ok: 

:::note alert
リロードまで体感10秒〜15秒かかりました。
遅い理由はわかっていないですが、似た事例の[記事](https://image.keikenchi.com/build-nextjs-development-environment-with-docker#google_vignette)を発見した。
いつか解消したい...。
:::

# 【まとめ】
ホットリロードを有効化するには、以下のことが必要。
1. docker-composeでボリュームマウントする
    1. 変更をコンテナ内部に反映させるため
1. 以下のオプションを明示的に有効にする
    1. `CHOKIDAR_USEPOLLING=true`
    1. `WATCHPACK_POLLING=true`

## 【おまけ】
`vite.config.ts` にホットリロードに必要な設定をした場合のものを置いておきます。
```vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vite.dev/config/
export default defineConfig({
  plugins: [react()],
  // サーバがリッスンするホストとポートを設定
  server: {
    host: true,
    port: 5173,
    // ホットリロードのために必要なオプション値
    watch: {
      usePolling: true,
      interval: 1000  
    }
  },
})
```