ASP.NET CoreでホットリロードするDocker開発環境を構築する方法

DockerでASP.NET Core (.NET 8) の開発環境を構築する機会がありました．そこで備忘として，実際に行った環境構築の方法をまとめます．

本記事は開発環境構築についてのご紹介ですが，本番環境構築時に行ったことについても後日まとめようと思います．

## 環境
- MacBook Air M2 (macOS Sequoia)
- .NET 8
- ASP.NET Core Blazor

:::note
環境にmacOSと記載していますが，Windows環境でも動作することを確認しております．
:::

## ディレクトリ構成
ASP.NET Coreプロジェクトのディレクトリ構成は以下のとおりです．一部不要なディレクトリ名やファイル名は記載していません．
```
src/
├ bin/
├ obj/
├ Components/
├ Properties/
│ └ launchSettings.json
├ appsettings.json
├ hogehoge.csproj
└ Program.cs
hogehoge.sln
```

## 1. Dockerfileの作成
非Docker環境でホットリロードありのデバッグを行う場合，CLI環境なら`csproj`ファイルのある階層で`dotnet watch run`すると思います．
これと同様のことをDockerでも行えば良いので，以下のようにDockerfileを構成します．
尚，Dockerfileは`sln`ファイルと同階層に配置します．
```Dockerfile:Dockerfile
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS base

ENV ASPNETCORE_ENVIRONMENT=Development

WORKDIR /app
COPY . ./

WORKDIR /app/src
RUN dotnet restore

ENTRYPOINT [ "dotnet", "watch", "run", "--no-restore" ]
```
### ポイント
- `FROM`句でベースとなる.NET SDKのコンテナレイヤをビルドします．
- `COPY . ./`でプロジェクトのディレクトリ全体をDockerコンテナの`/app`ディレクトリにコピーします．
- コンテナの`/app/src`ディレクトリにて`dotnet restore`により依存関係を復元したのち，`dotnet watch run --no-restore`でホットリロードありのデバッグを開始します．

## 2. docker-compose.ymlの作成
Dockerfileと同階層に，docker-compose.ymlファイルを作成します．
内容は以下のとおりです．
```docker-compose.yml
services:
    aspnetcore:
        build:
            context: .
            dockerfile: Dockerfile
        ports:
            - 8080:5000
        volumes:
            - ./src/.:/app/src
```
### ポイント
- `build: dockerfile:`には，docker-compose.ymlから見たDockerfileの相対パスを記載します．
- `ports:`にはWebアプリケーションのポートを記載します．`[ホスト側のポート]:[コンテナ側のポート]`の形式で記載します．
尚，`[コンテナ側のポート]`は後述するappsettings.jsonにて設定可能です．
- `volumes:`にてホスト側の`src`ディレクトリと，コンテナ側の`/app/src`ディレクトリをバインドしています．書式は`[ホスト側のパス]:[コンテナ側のパス]`です．
これによりホスト側のファイルを変更した際は，その内容が即座にコンテナ側のディレクトリに反映され，その後ホットリロードが実行されます．

## 3. appsettings.jsonの設定
appsettings.jsonに以下の設定を追加します．
```appsettings.json
"Kestrel": {
    "Endpoints": {
        "Http": {
            "Url": "http://*:5000"
        }
    }
}
```
ASP.NET CoreではKestrelというサーバーを利用します．そのエンドポイントとして，上記のように任意のURLを指定することができます．

### ポイント
- Dockerで構築する場合，`URL`のホスト名は自身を表す`localhost`や`127.0.0.1`ではなく，すべてのアドレスを示す`0.0.0.0`や`*`を記載します．その上で任意のポートを`:`で繋げてURLを構成します．
指定するポートはコンテナ側が待ち受けるポートになります．上記の例では，docker-compose.ymlで指定した5000番を指定しています．

## 4. いざコンテナ実行
docker-compose.ymlがあるディレクトリにて，以下コマンドを実行します．
```sh
docker compose up -d
```
少々時間がかかりますが，`http://localhost:[docker-compose.ymlで指定したホスト側ポート]`にアクセスしてWebアプリケーションの画面が表示されれば構築成功です（本記事の例では`http://localhost:8080`にアクセスします）．

コンテナやプロジェクトのビルドのログについては，Docker DesktopのコンテナのLogsにて確認することができます．

以上，DockerでのASP.NET Core開発環境構築の方法でした．

## 補足：ホットリロードが効かなくなる問題の対処法
ホットリロードできないような編集（[Rude編集](https://learn.microsoft.com/ja-jp/dotnet/core/tools/dotnet-watch#rude-edits)と呼ぶようです）を行った場合，`dotnet watch`は以下を表示し，コマンドの入力を求めてきます．
```
Unable to apply hot reload because of a rude edit.
Do you want to restart your app - Yes (y) / No (n) / Always (a) / Never (v)?
```

ただし，Docker環境ではこの入力ができないので，Rude編集があったとしても入力を求めないような設定が必要です．

その設定は，**コンテナに環境変数`DOTNET_WATCH_RESTART_ON_RUDE_EDIT`を新たに定義し，その値に`true`を指定する**ことで実現します．

環境変数はDockerfileに記載する，または別途`.env`ファイルに記載してそれをdocker-compose.ymlで指定する，等の方法で定義します．

ここでは上記の2通りの方法をご紹介します．開発するプロジェクトに適した方法を採用いただければと思います．

### Dockerfileに記載する方法
`ENV`命令を使用します．
```diff:Dockerfile
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS base

+ ENV DOTNET_WATCH_RESTART_ON_RUDE_EDIT=true
ENV ASPNETCORE_ENVIRONMENT=Development

WORKDIR /app
COPY . ./

WORKDIR /app/src
RUN dotnet restore

ENTRYPOINT [ "dotnet", "watch", "run", "--no-restore" ]
```

### `.env`ファイルに記載する方法
```.env
DOTNET_WATCH_RESTART_ON_RUDE_EDIT=true
```
このように記載した`.env`ファイルを作成し，docker-compose.ymlで以下のとおり指定します．
```diff:docker-compose.yml
services:
    aspnetcore:
        build:
            context: .
            dockerfile: Dockerfile
        ports:
            - 8080:5000
        volumes:
            - ./src/.:/app/src
+       env_file:
+           - <docker-compose.ymlから見た.envファイルの相対パス>           
```