ASP.NET Core のMacでのローカル開発を docker-compose で動かすまでのステップ

今回は ASP.NET Core MVC で試したが ASP.NET Core WebAPI プロジェクトでも同様のはず

## TL;DR
Microsoft のリファレンスにあるステップどうりに進めればOK。英語で読むことを推奨。
Windowsの手順もこっちに十分まとまっています。

- Docker images for ASP.NET Core
    - https://docs.microsoft.com/ja-jp/aspnet/core/host-and-deploy/docker/building-net-docker-images?view=aspnetcore-3.1
- Hosting ASP.NET Core images with Docker over HTTPS
	- https://docs.microsoft.com/ja-jp/aspnet/core/security/docker-https?view=aspnetcore-3.1
- Hosting ASP.NET Core images with Docker Compose over HTTPS
	- https://docs.microsoft.com/ja-jp/aspnet/core/security/docker-compose-https?view=aspnetcore-3.1

## なぜ書いたのか
- Microsoft のリファレンスがあちこちに飛び散っていて把握しづらかった
- 後述する自己署名証明書の部分で詰まって悲しかった
- 今回の作業したリポジトリは以下に格納してあります（作業中のリポジトリなので記事の部分以外のものが今後足されるかも）
    - https://github.com/JUNKI555/DotNETCoreIdentityPractice

## 対象環境など
- ローカル環境(Mac)
    - 無いと思いますが本番環境などに利用する際は証明書など適宜準備して下さい
- .NET Core 3.1
- 想定ディレクトリ構成
    - myapp
        - Controllers
        - Models
        - Views
        - wwwroot
        - appsettings.Development.json
        - myapp.csproj
        - Program.cs
        - Startup.cs
        - Dockerfile
        - docker-compose.yml

## 手順立ててやっていこう
一気にやろうとするとつまづいた時に戸惑うので順序立ててやっていくのは大事

### ステップ 1
ASP.NET MVC アプリなどを新規作成し、 `dotnet run` で起動確認

```bash
% cd myapp
% dotnet new mvc -o .
% dotnet run
```

※せっかく Docker 用意すんのにローカルPCに .NET Core SDK いるんかい！ という方は飛ばしてもOK

### ステップ 2
以下のような `Dockerfile` を用意し HTTP接続による Docker コンテナでの起動確認をしていく

```dockerfile
FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build
WORKDIR /app

# copy csproj and restore as distinct layers
COPY ./myapp.csproj .
RUN dotnet restore

# copy everything else and build app
COPY . ./
WORKDIR /app
RUN dotnet publish -c Release -o out

FROM mcr.microsoft.com/dotnet/core/aspnet:3.1.8-buster-slim AS runtime
WORKDIR /app
COPY --from=build /app/out ./
ENTRYPOINT ["dotnet", "myapp.dll"]
```

なお、 上記 Dockerfile は myapp.csprj と同一階層に Dockerfile を置いたときのサンプル。
Microsft のリファレンスでは以下のようなディレクトリ構造でのサンプルになっている
（.NET Core はプロジェクトを細分化してソリューションファイルでまとめて管理する開発方法が推奨されている? ので Microsoft のサンプルが正しいのだが今回は同一階層でやりたかった）

- myapp
	- aspnetapp
		- aspnetapp.csproj
- myapp.sln
- Dockerfile
- docker-compose.yml

Dockerfile を作成したら以下コマンドでHTTP接続による起動確認

```bash
% docker build -t myapp_image .
% docker run -it --rm -p 5000:80 --name myapp myapp_image
```

### ステップ 3
`dotnet dev-certs` コマンドを使用して自己署名証明書を準備する。

```bash
% dotnet dev-certs https -ep ${HOME}/.aspnet/https/aspnetapp.pfx -p { password here }
% dotnet dev-certs https --trust
```

`{ password here }` の部分を各々のパスワードに設定のこと。

本筋とは関係ないが今回ここの設定でミス（ `{ passward hogefuga }` など{}つきにしまっていた）していることに気づかず
自己署名証明書を使って立ち上げた時に以下の様なエラーが出て起動しなかった。

```bash
warn: Microsoft.AspNetCore.DataProtection.Repositories.FileSystemXmlRepository[60]
      Storing keys in a directory '/root/.aspnet/DataProtection-Keys' that may not be persisted outside of the container. Protected data will be unavailable when container is destroyed.
warn: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[35]
      No XML encryptor configured. Key {eebd890e-0312-4174-bb70-97f7873df627} may be persisted to storage in unencrypted form.
crit: Microsoft.AspNetCore.Server.Kestrel[0]
      Unable to start Kestrel.
Interop+Crypto+OpenSslCryptographicException: error:2006D080:BIO routines:BIO_new_file:no such file
   at Interop.Crypto.CheckValidOpenSslHandle(SafeHandle handle)
   at Internal.Cryptography.Pal.OpenSslX509CertificateReader.FromFile(String fileName, SafePasswordHandle password, X509KeyStorageFlags keyStorageFlags)
   at System.Security.Cryptography.X509Certificates.X509Certificate..ctor(String fileName, String password, X509KeyStorageFlags keyStorageFlags)
   at System.Security.Cryptography.X509Certificates.X509Certificate2..ctor(String fileName, String password)
   at Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader.LoadCertificate(CertificateConfig certInfo, String endpointName)
   at Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader.LoadDefaultCert(ConfigurationReader configReader)
   at Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader.Load()
   at Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServer.ValidateOptions()
   at Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServer.StartAsync[TContext](IHttpApplication`1 application, CancellationToken cancellationToken)
Unhandled exception. Interop+Crypto+OpenSslCryptographicException: error:2006D080:BIO routines:BIO_new_file:no such file
   at Interop.Crypto.CheckValidOpenSslHandle(SafeHandle handle)
   at Internal.Cryptography.Pal.OpenSslX509CertificateReader.FromFile(String fileName, SafePasswordHandle password, X509KeyStorageFlags keyStorageFlags)
   at System.Security.Cryptography.X509Certificates.X509Certificate..ctor(String fileName, String password, X509KeyStorageFlags keyStorageFlags)
   at System.Security.Cryptography.X509Certificates.X509Certificate2..ctor(String fileName, String password)
   at Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader.LoadCertificate(CertificateConfig certInfo, String endpointName)
   at Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader.LoadDefaultCert(ConfigurationReader configReader)
   at Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader.Load()
   at Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServer.ValidateOptions()
   at Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServer.StartAsync[TContext](IHttpApplication`1 application, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.Hosting.GenericWebHostService.StartAsync(CancellationToken cancellationToken)
   at Microsoft.Extensions.Hosting.Internal.Host.StartAsync(CancellationToken cancellationToken)
   at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.RunAsync(IHost host, CancellationToken token)
   at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.RunAsync(IHost host, CancellationToken token)
   at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.Run(IHost host)
   at myapp.Program.Main(String[] args) in /app/Program.cs:line 17
```

### ステップ 4
準備した自己署名証明書を使って Docker コンテナでの起動確認。
Docker イメージはステップ3で `docker build -t myapp_image .` して作ってあるはずなので以下。

```
% docker run --rm -it -p 5000:80 -p 5001:443 -e ASPNETCORE_URLS="https://+;http://+" -e ASPNETCORE_HTTPS_PORT=8001 -e ASPNETCORE_Kestrel__Certificates__Default__Password="password" -e ASPNETCORE_Kestrel__Certificates__Default__Path=/https/aspnetapp.pfx -v ${HOME}/.aspnet/https:/https/ --name myapp myapp_image
```

### ステップ 5
以下の様な docker-compose.yml を同一ディレクトリ上に作成し、起動確認。

```yml
version: '3.8'

services:
  webapp:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "5000:80"
      - "5001:443"
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - ASPNETCORE_URLS=https://+:443;http://+:80
      - ASPNETCORE_Kestrel__Certificates__Default__Password= { passward here }
      - ASPNETCORE_Kestrel__Certificates__Default__Path=/https/aspnetapp.pfx
    volumes:
      - ~/.aspnet/https:/https:ro
```

`{ password here }` の部分を先ほど設定した自己署名証明書のパスワードに変更のこと。

```bash
% docker-compose up
```

できた。
ここから先他のミドルウェアを docker-compose.yml に足していく方法などは .NET Core に限った話ではないので割愛。

### mcr.microsoft.com/dotnet/core/sdk:3.1-buster について
公式は
`mcr.microsoft.com/dotnet/core/sdk:3.1` のイメージを使っているが
いかんせんイメージがデカすぎるのでこちらを使用した。
他にも alpine イメージである `3.1-alpine` などもあるので気になる人は
DockerHub をみにいってみて下さい。

- .NET Core By Microsoft - DockerHub
	- https://hub.docker.com/_/microsoft-dotnet-core/
- .NET Core SDK - DockerHub
	- https://hub.docker.com/_/microsoft-dotnet-core-sdk/
- ASP.NET Core 2.1/3.1 Runtime - DockerHub
	- https://hub.docker.com/_/microsoft-dotnet-core-aspnet/

参考：

- Adding MySQL to ASP.NET Core App With Docker Compose
    - https://code-maze.com/mysql-aspnetcore-docker-compose/