【Day 29】バインドマウント(Bind Mount)によるソースコード即時反映(ホットリロード) 〜ホストとコンテナのディレクトリ直結、dotnet watch run による変更検知と自動再起動、WSL2ファイルイベント伝搬の注意点、快適なC#ローカル開発環境の構築〜
「C#コードを1行直すたびに docker compose build するの!? 待ち時間が長すぎて開発にならないよ!」
前回のDay 28では、SQL ServerのDBデータを安全に保護する「Named Volume(名前付きボリューム)」を学びました。
しかし、日々のアプリケーション開発現場で同じようにボリュームを扱おうとすると、多くのエンジニアが大きな壁にぶつかります。
「コードを修正するたびに、毎回イメージの再ビルド(docker compose up --build)を待たなければいけないのか?」
答えはもちろん NO です!
Dockerには、ホストPC上のソースコードフォルダをコンテナ内に直接パイプで繋ぐ 「バインドマウント(Bind Mount)」 という強力な仕組みが用意されています。
さらにC#の dotnet watch コマンドと組み合わせることで、「ホストのVisual StudioやVS Codeでコードを保存(Ctrl+S)した瞬間、コンテナ内のアプリがミリ秒で自動更新されるホットリロード環境」 が実現します!
本講義では、バインドマウントの原理、docker-compose.yml の設定法、そしてWSL2環境で多くの人がハマる「変更検知トラブル」の完全解決策までを徹底解説します!
INDEX 本日の講義カリキュラム
- 第1章: バインドマウント(Bind Mount)の基本原理 〜ホストとコンテナを直結するパイプ〜
- 第2章: docker-compose.yml でのバインドマウント記法と「bin/obj上書き問題」の回避策
- 第3章: C# / ASP.NET Core の秘密兵器「dotnet watch」とホットリロードの仕組み
- 第4章: 実践!Docker Compose + dotnet watch で超高速C#ホットリロード環境を構築する
- 第5章: WSL2環境で絶対にハマる「ファイル変更が検知されない」問題の真相とinotify対策
- 第6章: 現場での使い分け決定版!「Named Volume」vs「Bind Mount」
- 第7章: 本日のまとめと理解度チェッククイズ(次回予告:Day 30 Git管理とVolumeの境界線)
第1章: バインドマウント(Bind Mount)の基本原理 〜ホストとコンテナを直結するパイプ〜
これまでの講義で学んだ通り、コンテナは通常、Dockerfileの COPY 命令によってビルド時にイメージ内部へファイルを焼き込みます。
そのため、ホストPC上でC#コードを修正しても、コンテナ内のファイルには一切反映されず、再度 docker build を実行して新しいイメージを作成する必要がありました。
これに対して 「バインドマウント(Bind Mount)」 とは、ホストPC上の特定フォルダを、コンテナ内部の特定ディレクトリへ「そのまま直接マウント結合」する技術 です。
前回のNamed Volumeが「Docker Engineの管理領域(安全な金庫)をコンテナに繋ぐ」ものだったのに対し、バインドマウントは 「開発者が日常的にGitで管理している作業ツリー(./srcなど)をそのままコンテナに露出させる」 という決定的な違いがあります。
上図をご覧ください。ホストPC側の ./src フォルダとコンテナ内部の /app ディレクトリが直結されています。
この接続により、以下の劇的なメリットが生まれます:
- 双方向の完全リアルタイム同期: ホスト側のVisual StudioやVS Codeでファイルを保存した瞬間、コンテナ内のファイルも即座に書き換わります。
- Gitとの親和性: ホスト側のフォルダは通常のGit管理下にあるため、ブランチの切り替えやコミット操作が手元のGitツールでそのまま行えます。
- ビルド待ち時間の全廃: コードを書き換えるたびに
docker buildする数分間のロスタイムが完全にゼロになります。
第2章: docker-compose.yml でのバインドマウント記法と「bin/obj上書き問題」の回避策
バインドマウントを docker-compose.yml で記述する場合、volumes: セクションに 「ホストの相対パス : コンテナ内の絶対パス」 を指定します。
# docker-compose.yml の基本バインドマウント記法 services: webapi: build: context: . dockerfile: Dockerfile.dev ports: - "5000:5000" volumes: # ホストの ./src フォルダをコンテナ内の /app/src にバインドマウント - ./src:/app/src
しかし、C# / .NET 開発者がここで必ず直面するのが、悪名高い 「ホストとコンテナの bin / obj 衝突問題」 です!
⚠️ C#エンジニア必読:bin / obj 衝突の罠
ホストPC(Windows)でVisual Studioを開いていると、Windows用のバイナリが bin/ や obj/ にビルドされます。
これをそのままLinuxコンテナに丸ごとバインドマウント(.:/app)してしまうと、Windows用のDLLやビルドキャッシュがコンテナ内のLinuxランタイムと衝突し、ビルドエラーや不可解な例外でアプリが起動しなくなります!
この問題をエレガントに解決するプロの技法が、「匿名ボリューム(Anonymous Volume)によるマスキング」 です:
# bin / obj の衝突を回避する黄金のボリューム定義 services: webapi: volumes: - .:/app # ソースコード全体をマウント - /app/bin # コンテナ内の bin を独立ボリュームで保護(ホストで上書きさせない) - /app/obj # コンテナ内の obj を独立ボリュームで保護
Dockerはマウントパスが重複する場合、「より深い階層のマウントを優先する」という仕様を持っています。
上記のように /app/bin と /app/obj をコロンなしの「匿名ボリューム」として指定することで、ソースコード(.csファイル等)はホストと直結されつつ、ビルド生成物(bin/obj)はLinuxコンテナ独自の専用領域に隔離される のです!
第3章: C# / ASP.NET Core の秘密兵器「dotnet watch」とホットリロードの仕組み
ファイルをバインドマウントしただけでは、コンテナ内のプロセスは起動時の古いDLLを実行し続けてしまいます。
そこで登場するのが、.NET CLIに標準搭載されている dotnet watch コマンドです!
dotnet watch は、指定したディレクトリ内のファイルシステム監視イベントを常時リッスンします。
上図の3ステップの流れをご覧ください:
- Step 1: ホスト側でコード編集&保存: 開発者がVisual StudioやVS CodeでC#コードを修正し、保存(Ctrl+S)します。
- Step 2: バインドマウント経由で変更検知: ホストOSのファイル更新がバインドマウントを通じて即座にコンテナ内のLinuxカーネルに通知されます。
- Step 3: 差分のみ自動反映(ホットリロード):
dotnet watchが変更を検知し、メソッドの追加やロジック変更であれば 「プロセスを再起動することなく、実行中メモリ内のILコードを瞬時に差し替え(ホットリロード)」 します!
構造の大きな変更(クラスの継承関係変更やプロジェクトファイルの変更など)が発生した場合でも、dotnet watch が自動的にプロセスを再起動(Restart)してくれるため、開発者がターミナルに触る必要は一切ありません。
第4章: 実践!Docker Compose + dotnet watch で超高速C#ホットリロード環境を構築する
それでは、実際に現場で使える完全な設定ファイルを見ていきましょう。
本番用Dockerfile(軽量なaspnetランタイムを使用)とは別に、開発専用の Dockerfile.dev を用意するのがベストプラクティスです。なぜなら、ホットリロードを実行するには .NET SDK が必要だからです。
続いて、docker-compose.yml です:
これで準備完了です!ターミナルから以下のコマンドを実行してみましょう:
起動後、ブラウザで http://localhost:5000/weatherforecast にアクセスします。
その後、手元のIDEでコントローラー内の返却文字列を少し書き換えて保存(Ctrl+S)してみてください。
ターミナル上に Hot reload succeeded in 82 ms. と表示され、ブラウザをF5リロードするだけで新ロジックが即座に反映されるはずです!
第5章: WSL2環境で絶対にハマる「ファイル変更が検知されない」問題の真相とinotify対策
Windows 11 + WSL2環境でDockerを使っているエンジニアが、ほぼ100%と言っていい確率でハマる 「魔の落とし穴」 があります。
それは、「コードを保存したのに、コンテナ内の dotnet watch がうんともすんとも言わない(変更を検知しない)」 という現象です!
この原因は、「プロジェクトファイルをWindows領域(Cドライブ直下など)に配置していること」 にあります。
上図の対比をご覧ください:
🐧 Linux領域(超推奨:wsl$\...)
- 配置先:
/home/username/projects/... - Linuxネイティブの
inotifyイベントが即座にDocker Engineへ到達。 - ファイルI/O速度がWindows領域の約5倍〜10倍高速!
- ホットリロードが100%確実に瞬時動作。
🐢 Windows領域(非推奨:/mnt/c/...)
- 配置先:
C:Users ame\projects\... - 9Pプロトコル経由のため、Windowsのファイル更新イベントがWSL2内で欠落・遅延する。
- OS境界をまたぐためディスクI/Oが著しく低速。
- ホットリロードが反応しないトラブルが多発!
💡 もしどうしてもCドライブ配下で開発したい場合の特効薬
会社の制約等でプロジェクトをWindowsファイルシステム上に置かざるを得ない場合は、前章の環境変数:
DOTNET_USE_POLLING_FILE_WATCHER=true
を必ず設定してください。OSのイベント通知に頼る代わりに、.NET自身が一定間隔でファイル差分をポーリング監視(確認)するため、Cドライブ上でも確実に変更を検知できるようになります!
第6章: 現場での使い分け決定版!「Named Volume」vs「Bind Mount」
第6部で学んだ2大ストレージ機能、「Named Volume」と「Bind Mount」。
現場でどちらを使うべきか迷ったら、以下の 「3大黄金基準」 で判断してください:
| 機能 | 主な用途 | アクセス元 | パフォーマンス&権限 |
|---|---|---|---|
| Named Volume | SQL Server / PostgreSQL / Redis などの永続データ | コンテナのみ(ホストからは不可視) | 最高速(Ext4)。権限競合なし。安全保護。 |
| Bind Mount | C#ソースコード、設定ファイル(appsettings等) | ホストのIDE + コンテナの双方 | ホスト依存。ホットリロード・即時同期用。 |
上図の通り、現代のDocker × C#開発現場では、「データベースにはNamed Volumeをあてがい、アプリケーションコードにはBind Mountを適用する」 というハイブリッド構成がデファクトスタンダードです。
これにより、DBのレコードは安全に守られつつ、アプリコードは保存即リロードという、究極の快適開発環境が手に入ります!
第7章: 本日のまとめと理解度チェッククイズ
📌 本日の最重要チェックポイント
- バインドマウントはホストフォルダの直結パイプ:
COPYによる再ビルド不要で、手元のIDEの編集がコンテナへ即時反映。 - C#のホットリロードは
dotnet watchで実現: 差分ILコードの差し替えにより、コンテナ再起動なしでミリ秒更新。 bin/objは匿名ボリュームで必ず隔離: ホスト(Windows)とコンテナ(Linux)のバイナリ衝突を防ぐ必須テクニック。- WSL2ではプロジェクトをLinux領域に置くのが鉄則:
wsl$Ubuntu\...配下に置くことでinotifyが高速伝搬。 - コードはバインドマウント、データはNamed Volume: 役割に応じた適切な使い分けがプロの現場設計。
🎯 理解度チェッククイズ
Q: Windows 11上でDocker Composeと dotnet watch を用いてC#開発を行う際、コードを保存してもホットリロードが反応しない場合の対策として、最も適切なものはどれでしょうか?
- A. コンテナを特権モード(
privileged: true)で起動する - B. プロジェクトをLinux領域(
wsl$\...)に移動するか、環境変数DOTNET_USE_POLLING_FILE_WATCHER=trueを設定する - C. Dockerfile で
USER rootを指定して実行権限を最大化する - D.
dotnet watchではなくdotnet runを使用する
正解と解説を見る
正解: B
WindowsのNTFS領域(/mnt/c/)からWSL2へのファイル変更イベント(inotify)は遅延や欠落が発生しやすいため、ネイティブLinuxファイルシステム領域(wsl$\...)にプロジェクトを配置することが最も根本的な解決策です。また、設定で DOTNET_USE_POLLING_FILE_WATCHER=true を有効にすることで、ポーリング方式により確実に変更が検知されるようになります。
🚀 次回予告(Day 30)
次回は第6部の総決算!「Git管理すべきファイルとVolumeに任せるデータの境界線」 を徹底解説します!
Gitにコミットすべき設定ファイル、.gitignore で除外すべきデータ、そしてVolumeで永続化すべきデータベースの境界線を、C#開発現場の実例をもとに完璧に整理します。お楽しみに!
コメント
コメントを投稿