ASP.NET Core のWebアプリケーションを docker-compose で Dockerアプリケーション として構築する

# 概要
ASP.NET CoreのサンプルWebアプリケーションを題材として、docker-compose による Dockerアプリケーション の動作環境を構築する例を紹介します。

# 更新履歴
- 2023/07/01：各種、ソフトウェア・ミドルウェアのバージョンを更新しました。 

# 環境
- Windows11
- Visual Studio 2022
- ASP.NET Core (.NET6)
- Docker for Windows

# サンプルアプリケーション
題材にしたサンプルアプリケーションです。
https://github.com/tYoshiyuki/dotnet-core-web-sample

単純なToDoのWebアプリケーションです。トランザクションデータはデータベース (SQL Server) に保存しています。
これを Docker を利用して コンテナ化 してみましょう。

# システム構成
今回は、Docker for Windows を利用して、ローカルPC上にDocker環境を構築します。ローカル開発時は、IIS Express と LocalDB を利用して開発を行いましたが、これを変更します。フロントエンドは Nginx、データベースは SQL Express の Dockerイメージを利用し、アプリケーションのコンテナを含めて3つのコンテナで構築します。構成イメージは下記の通りです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/353069/247232c2-9296-a239-29ed-8fe8fc695b98.png)

通常、ASP.NET Core アプリケーションを IIS でホストする場合には、ASP.NET Coreのミドルウェアにより、アプリケーションはIISと統合的に動作します。Nginxをリバースプロキシとしてフロントエンドに構築する場合は、ASP.NET Core アプリケーションは Kestrel で動作させておき、そこに対してHTTP通信を転送します。

# Dockerfile

## 1. ASP.NET Core アプリケーション
まずは、ASP.NET Core アプリケーションをコンテナ化するために、Dockerfileを作成します。`Visual Studio > プロジェクト > 右クリック Dockerサポート > ターゲットOS Linux` から追加できる Dockerfile をベースにして編集を行います。

```dockerfile:Dockerfile
FROM mcr.microsoft.com/dotnet/aspnet:6.0.1-bullseye-slim AS base

WORKDIR /app
# EXPOSE 80
# EXPOSE 443

FROM mcr.microsoft.com/dotnet/sdk:6.0.101-bullseye-slim AS build
WORKDIR /src
COPY ["DotNetCoreWebSample.Web.csproj", "./"]
RUN dotnet restore "DotNetCoreWebSample.Web.csproj"
COPY . .
WORKDIR "/src/."
RUN dotnet build "DotNetCoreWebSample.Web.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "DotNetCoreWebSample.Web.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENV ASPNETCORE_URLS http://*:5000
ENTRYPOINT ["dotnet", "DotNetCoreWebSample.Web.dll"]
```

Dockerfileの解説についてはここでは省略しますが、デフォルトでマルチステージビルドを行い、Dockerイメージサイズを削減するような設定になっています。ポイントとしては `[ENV ASPNETCORE_URLS http://*:5000]` で、Kestrelが待ち受けを行うポート番号を指定している部分です。後々、Nginxでリクエストを転送する際における、送信先ポート番号になります。

また、ソースコードも公式ドキュメントに従い、若干ですが変更を行います。
https://docs.microsoft.com/ja-jp/aspnet/core/host-and-deploy/linux-nginx?view=aspnetcore-3.1

```c#:Startup.cs
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            // ・・・一部省略・・・

            app.UseForwardedHeaders(new ForwardedHeadersOptions
            {
                ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
            });

            // ・・・一部省略・・・
```

リバースプロキシを経由した際における、X-Forwarded-For・X-Forwarded-Protoヘッダの転送を行うようにします。
これは、IIS上でASP.NET Coreアプリケーションを動作させた場合は、既定で有効になっているので注意が必要です。

公式ドキュメントにも記載があるため、目を通しておくことをお勧めします。
https://docs.microsoft.com/ja-jp/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-3.1

## 2. Nginx
次にNginx用の設定を作成します。

```dockerfile:Dockerfile
FROM nginx:latest

COPY ./nginx.conf /etc/nginx/nginx.conf

COPY ./site.conf.template /etc/nginx/templates/site.conf.template

CMD [ "nginx", "-g", "daemon off;" ]
```

```nginx:nginx.conf
worker_processes 1;

events { worker_connections 1024; }

http {
    sendfile on;

    include /etc/nginx/conf.d/site.conf;
}
```

```site.conf.template
upstream web-app {
    server ${BACKEND_HOST};
}

server {
    listen 80;
    server_name $hostname;
    location / {
        proxy_pass         http://web-app;
        proxy_redirect     off;
        proxy_http_version 1.1;
        proxy_cache_bypass $http_upgrade;
        proxy_set_header   Upgrade $http_upgrade;
        proxy_set_header   Connection keep-alive;
        proxy_set_header   Host $host;
        proxy_set_header   X-Real-IP $remote_addr;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto $scheme;
        proxy_set_header   X-Forwarded-Host $server_name;
    }
}
```

nginx 1.19 の Dockerイメージ より、環境変数を template を用いることで注入することが出来るようになりました。
この機能を用いて、アプリケーションサーバの宛先 (BACKEND_HOST) を外部注入可能にしておきます。これに関しては、後ほど docker-compose.yml で解説を行います。

## 3. SQL Server Express
最後に SQL Server Express です。サンプルアプリケーションの機能としてはLocalDBで十分なのですが、公式にDockerイメージが存在しないようなので、SQL Server Expressを利用します。尚、実際にはDBデータを永続化する設定が必要ですが、今回は省略しています。

```dockerfile:Dockerfile
FROM mcr.microsoft.com/mssql/server:2017-latest-ubuntu
```

# docker-compose.yml
作成した各コンテナを docker-compose を利用して起動してみましょう。

```yaml:docker-compose.yml

version: '3.4'

services:
  app:
    build:
      context: ./DotNetCoreWebSample.Web
      dockerfile: Dockerfile
    environment:
      ConnectionStrings__DefaultConnection: "Server=sqlexpress;Database=master;User ID=sa;Password=P@ssw0rd;initial catalog=dotnetcorewebsample;MultipleActiveResultSets=True;App=EntityFramework;"
    expose:
      - 5000
    depends_on:
      - sqlexpress

  sqlexpress:
    build:
      context: ./docker/sqlexpress
      dockerfile: Dockerfile
    environment:
      MSSQL_PID: "Express"
      ACCEPT_EULA: "Y"
      SA_PASSWORD: "P@ssw0rd"
    ports:
      - 1433:1433

  web:
    build:
      context: ./docker/nginx
    environment:
      BACKEND_HOST: "app:5000"  
    ports: 
      - 80:80

```

記載が長いですが、何点かポイントを解説します。

まず、 ` ConnectionStrings__DefaultConnection: "Server=sqlexpress;Database=master;User ID=sa;Password=P@ssw0rd;initial catalog=dotnetcorewebsample;MultipleActiveResultSets=True;App=EntityFramework;" ` の部分は、SQL Server Express への接続先情報を設定しています。コンテナ起動時に appsettings.json で設定している内容を上書きします。ネストされたJSONの要素 (ConnectionStrings > DefaultConnection) を表現する場合に、アンダーバー2つ (__) を使用します。

` BACKEND_HOST: "app:5000" ` の部分で、ASP.NET Core アプリケーションへの送信先を設定しています。docker-composeでは、サービス名で各コンテナの宛先を解決出来るため、上記のような設定になります。

docker-composeコマンドを実行し、各コンテナを起動します。

```bash
docker-compose up -d
```

実行後 `http://localhost` でアクセスし、動作を確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/353069/bc01a2cf-d003-61a7-ad2e-91d5d26e3b9a.png)

# まとめ
docker-composeを用いて、Dockerアプリケーションを構築する例を紹介させていただきました。
クロスプラットフォームを特徴とする .NET Core なので、Dockerを用いた開発も盛り上がると良いなと思います。

ASP.NET Core と Azure で開発する場合、Web Apps や SQL Database といった優れた PaaS を利用することで、手軽にスケーラブルなアプリケーションを構築することが出来ます。そのため、Docker のようなコンテナ技術を利用するシーンは、あまり多く無いかも知れません。

ですが、ASP.NET Coreのアプリケーションをコンテナ化することで、AWS や GCP といった別クラウドサービスへの転用や、Kubernetesに代表される コンテナ オーケストレーション の恩恵を受けることが出来るようになります。システムアーキテクチャの選択肢として、考慮する価値は十分にあるのではないのでしょうか。