はじめに
DockerもBlazorも、ほぼ触ったことがないけど、BlazorをDockerで動してみたい
データベースも含めてdocker-compose
する
具体的には公式チュートリアルのTodoアプリを発展させて、アイテムを永続化(DBから読み書き)したい
環境
- Windows 10 Home
- Visual Studio 2022 Community
- .NET 6
- SQL Server 2019
- Docker version 20.10.13, build a224086
- Docker Compose version v2.3.3
Blazorプロジェクトをつくる
dotnet new
の引数(こちらも)を参考に、プロジェクトをつくる
dotnet new blazorwasm --hosted -o ./project -n BlazorHosted
オプション | 意味 |
---|---|
-o |
プロジェクトを配置する場所 |
-n |
作成されるプロジェクトの名前 |
--hosted |
ASP.NET Core ホストが含まれる |
ベースとなるコード
BlazorでTodoアプリを作る公式チュートリアルを参考にして、Razorページとモデルをプロジェクトに追加する
この時点で実行すると、ナビゲーションにTodoが追加されて、任意のアイテムを追加できるようになっている
ただし、データの永続化については未着手なので、リロードするたびにデータは初期状態(空のデータ)になる
※Visual Studioからデバッグ実行してitem1
をAdd todo
した時の様子
Tips
dotnet watch run
しておくとデバッグが楽
2022/04/16追記
dotnet watch
のみでいいかも?
データベースをつかう準備
アプリ側でデータベースをつかう準備をする
去年のアドベントカレンダーにて、参考になる記事がある
NuGetのパッケージをインストールする
SQL Serverを扱うために、必要なライブラリを揃える
Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.Design
Microsoft.EntityFrameworkCore.SqlServer
Microsoft.EntityFrameworkCore.Tools
データベースコンテキストの中身や、シードデータの与え方は、手前味噌過去記事参照
体系立ててまとめられておらず、我ながら情報が探しにくい
データベースに接続する設定をする
.NET 6でつくったので、いろいろ過去の記事と齟齬が生じている
(たとえばStartup.cs
ファイルが無いなど)
サーバー側のProgram.cs
.NET 6でプロジェクトをつくると、丁寧に// Add services to the container.
とガイドされているので、そこにサービスを追加する
builder.Services.AddDbContext<BlazorHosted.Server.Context.TodoDbContext>(
options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
appsettings.json
に接続文字列を追加する
デバッグ中はlocaldb
をつかう
dockerで動かすときは、docker-compose.yml
で接続文字列を上書きする(後述)
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=BlazorAppDb;Trusted_Connection=True;",
},
過去記事ではまったポイントとして、パスワードが8文字以下であるとうまくいかないので注意する
モデルを変更する
チュートリアルでつくったTodoItem.cs
を変更する
データベースに格納するので、Id
を追加する
namespace BlazorHosted.Shared;
public class TodoItem
{
public int Id { get; set; }
public string? Title { get; set; }
public bool IsDone { get; set; }
}
慣例により、Id または Id という名前のプロパティがエンティティの主キーとして構成されます。(引用元)
DBコンテキストを新規作成する
サーバー側にDBコンテキストを生成する
これにより、EF CoreのツールからDBを新規作成したり、テーブルの定義を更新できたりする
シードデータとして2つ先にデータを用意しておく
using Microsoft.EntityFrameworkCore;
using BlazorHosted.Shared;
namespace BlazorHosted.Server.Context
{
public class TodoDbContext : DbContext
{
public TodoDbContext(DbContextOptions<TodoDbContext> options) : base(options) { }
public virtual DbSet<TodoItem> TodoItems { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<TodoItem>().HasData(
new TodoItem { Id = 1, Title = "item1st", IsDone = true },
new TodoItem { Id = 2, Title = "item2nd", IsDone = false }
);
}
}
}
スキャフォールディングでコントローラーを追加する
サーバー側のコントローラーを右クリックして、"コントローラー"を追加する
クライアント(ウェブブラウザ)からサーバーにGETやPOSTなどリクエストがあった場合の挙動を記すのがコントローラー
(じっさいのところ、ユーザーはボタンを押すだけでGETなどは意識しないが・・)
APIのEFをつかったアクション(GET,POSTなど)があるコントローラーをえらぶ
今さらながら、モデルはSharedに、コンテキストはサーバーに、それぞれ配置した
はまったポイント:GETしたらAPIの生の結果がかえってきた
テストのためにlocalhost/todo
アクセスすしたところ、ページではなくAPIの結果が生でかえってきた
解決した
先述の記事のコードではそんなことなかったので、比較してみたところ
戻値に違いがあった
// 自分のコード
[HttpGet]
public async Task<ActionResult<IEnumerable<TodoItem>>> GetTodoItems()
{
return await _context.TodoItems.ToListAsync();
}
// こちらが正しい
[HttpGet]
public async Task<ActionResult<IEnumerable<TodoItem>>> GetTodoItems()
{
var items = await _context.TodoItems.ToListAsync();
return Ok(items);
}
修正したところ、ちゃんとLoading...
が表示されるようになった
Razor Page を変更する
こちらもチュートリアルから少し変更をくわえる
- アイテムがまだnullの状態であれば、その旨を表示しておく
-
OnInitializedAsync
を追加して、DBにすでに存在するデータを表示する - アイテムが追加されたらDBに書き込む
@page "/todo"
@using BlazorHosted.Shared
@using System.Linq;
@inject HttpClient Http
<PageTitle>Todo</PageTitle>
@if (todos == null)
{
<p>No Todo Items found.</p>
}
else
{
<h1>Todo (@todos.Count(todo => !todo.IsDone))</h1>
<ul>
@foreach (var todo in todos)
{
<li>
<input type="checkbox" @bind="todo.IsDone" />
<input @bind="todo.Title" />
</li>
}
</ul>
<input placeholder="Something todo" @bind="newTodo" />
<button @onclick="AddTodo">Add todo</button>
}
@code {
private List<TodoItem>? todos;
private string? newTodo;
private void AddTodo()
{
if (!string.IsNullOrWhiteSpace(newTodo))
{
TodoItem newTodoItem = new() { Title = newTodo };
todos?.Add(newTodoItem);
newTodo = string.Empty;
Http.PostAsJsonAsync<TodoItem>(requestUri: "api/TodoItems", value: newTodoItem);
}
}
protected override async Task OnInitializedAsync()
{
todos = await Http.GetFromJsonAsync<List<TodoItem>>(requestUri: "api/TodoItems");
}
}
データベースをマイグレーションする
データベースをつくる
Add-Migration Init
Update-Database
動作している様子
- 2つのアイテムがはじめから追加されている
- 更新してもアイテムが残っている
以上がdockerをつかわないデバッグまで
移行の適用によると、バンドルを使う方法が比較的あたらしいらしい
本番環境では安易にコマンド一発でDBを更新するのではなく、SQLスクリプトを出力して中身を精査してから実行すべし。
ということで、dockerfileではそのようにして移行する
dockerfileとcompose.ymlを準備する
ここからdockerで動かすためのファイルを準備する
dockerfile
ASP.NET Core 向けの Docker イメージを参考にした
こちらやこちらも参考にした
命令 | 意味 |
---|---|
FROM | 以降の命令で使う ベース・イメージ。AS で別名をつけられる |
WORKDIR |
RUN やCMD , ENTRYPOINT が実行される場所 |
COPY | コピー元はdockerfileが置かれいてるカレントパス. 、コピー先はWORKDIR からの相対パス。コピー先を. とした場合は、すなわちWORKDIR を指す。 |
主な命令の一覧はこちらに掲載されている
。。。と、自分で書くのも勉強になるが、ミスを連発しそうなのでVisual Studioの機能を使う
マルチステージビルドなる方法により、最終的なイメージサイズが削減している
#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["Server/BlazorHosted.Server.csproj", "Server/"]
COPY ["Shared/BlazorHosted.Shared.csproj", "Shared/"]
COPY ["Client/BlazorHosted.Client.csproj", "Client/"]
RUN dotnet restore "Server/BlazorHosted.Server.csproj"
COPY . .
WORKDIR "/src/Server"
RUN dotnet build "BlazorHosted.Server.csproj" -c Release -o /app/build
FROM build AS publish
ENV PATH="$PATH:/root/.dotnet/tools"
RUN dotnet publish "BlazorHosted.Server.csproj" -c Release -o /app/publish
# ここから
RUN dotnet tool install --global dotnet-ef --version 6.0.3
RUN dotnet ef migrations bundle -o /app/publish/bundle
RUN chmod +x /app/publish/bundle
# ここまでを追加
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "BlazorHosted.Server.dll"]
publishのところでapp/publish
直下にbundle
ファイルを生成している
docker compose up
したあとにこのバンドルをつかってDBをマイグレーションする
docker-compose.yml
ConnectionString
より上まではVisual Studioが生成してくれている
-
ConnectionString__
で接続文字列を上書きしている -
ports
は8080を指定しているが、VS Codeからup
しても8080以外のポートが接続先になることがある(よくわからない) - DBは十分に長いパスワードを設定しないと正常にたちあがらない
- ポート11433へSSMSからもアクセスできる
version: '3.4'
services:
blazorhosted.server:
image: ${DOCKER_REGISTRY-}blazorhostedserver
build:
context: .
dockerfile: Server/Dockerfile
environment:
- ConnectionStrings__DefaultConnection=Server=db;Database=master;User=sa;Password=SqlPass1234;
ports:
- '127.0.0.1:8080:80'
- '127.0.0.1:10443:443'
depends_on:
- db
db:
image: "mcr.microsoft.com/mssql/server:2019-latest"
environment:
- SA_PASSWORD=SqlPass1234
- ACCEPT_EULA=Y
ports:
- 11433:1433
version
とは?
version
から何を指定すればよいかわからない
-
公式のGet Startedでは
3.9
をつかっている - 3系と2系があるらしい
-
表にdockerエンジンとの対応が書かれている
- 自分のdockerは
ocker version 20.10.13
なので、とりあえず最新のバージョンでもよさそう
- 自分のdockerは
ということで、docker run
のオプションを参考にyml
ファイルを書いてみる
前記事から引き続き、こちらの記事も参考にさせていただいた
ビルドに必要なイメージをpull
する
SQL Server 2019のイメージを取得しておく --->>> 過去記事
Microsoftのdocker-hubから必要なイメージをpull
する
docker pull mcr.microsoft.com/dotnet/sdk:6.0
docker pull mcr.microsoft.com/dotnet/aspnet:6.0
docker images
で確認すると、イメージが追加されている
> docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
mcr.microsoft.com/dotnet/sdk 6.0 2d1a2c7481f5 11 days ago 721MB
mcr.microsoft.com/dotnet/aspnet 6.0 683c56113596 11 days ago 208MB
mcr.microsoft.com/mssql/server 2019-latest 56beb1db7406 8 months ago 1.54GB
.yml
の書き方について:こちらの記事に各リファレンスへのリンクもまとまっている
イメージを作成して起動する
自作Blazorプロジェクトのイメージをつくる
ビルドにはそれなりの時間がかかるので、Visual Studioであらかじめよくデバッグしておく
自分のPCでは[+] Building 1070.4s (23/23) FINISHED
とのことで、ビルドに1070秒、15分以上かかった・・・(これはどうなんだ?)
# .ymlファイルがある階層で build->up する
cd ./project
docker compose build
docker compose up -d
生成物を確認する
# イメージ
> docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
blazorhostedserver latest 7a9a0dc4eabd 3 minutes ago 464MB
...後略...
# コンテナー
> docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
403182a89b2c blazorhostedserver "dotnet BlazorHosted…" 16 seconds ago Up 7 seconds 127.0.0.1:8080->80/tcp, 0.0.0.0:63927->443/tcp project-blazorhosted.server-1
376c37326b66 mcr.microsoft.com/mssql/server:2019-latest "/opt/mssql/bin/perm…" 17 seconds ago Up 12 seconds 0.0.0.0:11433->1433/tcp project-db-1
現時点ではDBにテーブルができていないので、サイトにアクセスしてもTodoページはエラーが表示される
DBマイグレーションする
dockerfileでbundleを書き出してある。エントリーポイントを書きかえて実行する
一時的にしか使わないので--rm
をつけておく
docker compose run --rm --entrypoint /app/bundle blazorhosted.server
バンドルは、.NET SDK または EF ツールを (または、自己完結型の場合は .NET ランタイムさえも) インストールせずに実行でき、プロジェクトのソース コードは必要ありません。
余計な(?)ものを環境に入れなくても、バンドルファイルさえあればよい
サイトにアクセスする
ブラウザからlocalhost:8080
にアクセスすると、Blazorページが表示される
※タイミングによって?は8080ではないポートがバインドされている。ここはあとでよく調べないと・・・
VS Codeの左ペインのdockerアイコンを押すと、コンテナーの一覧が表示されるので、そこからブラウザを開く
やはり8080ポートではなかったものの、TodoページにはシードしたDBのデータが表示された
後片付け
コンテナーを破棄
docker compose down
イメージを破棄
docker rmi blazorhostedserver
おわりに
以下、まだ腑に落ちないところがあるが、とりあえずやりたいことができた
- docker-compose.ymlでportを8080:80などと指定しても、ちがうポートが開く
- docker compose buildに15分かかる
追記(22' 4/11)
http://localhost:8080
でページが表示されないのは、安全が確保されていないからっぽい
コンテナーが立ち上がっていないわけではないっぽい
参考にさせていただいたサイトなど
- MS公式docsと付随するgithubリポジトリ
- チュートリアル: ASP.NET Core で Web API を作成する
- docker docsに、おあつらえ向きのページがあった
- Razor Pagesに関するチュートリアルも参考になる
また、上記オフィシャルな記事を補完してくださる記事