前回【Day 21】では、複数のコンテナを1行でまとめて動かす「Docker Compose」の全体像と宣言的YAMLの基本を学びました。
そして本日【Day 22】は、C#/.NETエンジニアにとって最も実務直結であり、最も頻出する黄金の組み合わせ「ASP.NET Core Web API + Microsoft SQL Server」のマルチコンテナ連携ハンズオンに挑みます!
開発者PCを重厚なSQL Serverのインストールで汚す悪夢から完全に脱却し、
docker compose up -d のたった1コマンドで、完全なデータベース付き開発環境を一瞬で立ち上げるモダンな開発フローを体感しましょう!
① 導入・本日の達成目標
「プロジェクト参画初日、開発PCにSQL Server Developer Editionをインストールするだけで半日終わった…」
「同僚のPCでは動くのに、私のPCではSQLの照合順序(Collation)や大文字小文字の区別設定が違っていてクエリエラーになる…」
「ローカルのSQL Serverサービスが常駐しているせいで、ノートPCのメモリが圧迫されてVisual Studioの動作がもっさりする…」
長年Windows環境でC#開発に携わってきたベテランエンジニアほど、このような「ローカルDB個別インストール地獄」に苦しめられた経験があるはずです。
しかし、現在マイクロソフトは公式のLinux版SQL Serverコンテナイメージ(mcr.microsoft.com/mssql/server)を無償提供しており、Docker Composeを使えばローカルPCにSQL Server本体を1バイトもインストールすることなく、本番と100%同一のDB環境を即座に手に入れることができます!
本日の講義を修了すると、以下の3大目標を完全に達成できます。
- 公式SQL Serverコンテナの定義(SA_PASSWORD, ACCEPT_EULA, volumes)を習得し、安全なデータ永続化とライセンス管理を実現できる。
- Compose内部DNSによる名前解決(
Server=db,1433)を理解し、Entity Framework Core(EF Core)の接続文字列を環境変数経由で正しく注入できる。 - 単純な
depends_onでは防げない起動時クラッシュを、healthcheckによる「完全準備完了(Ready)待ち合わせ」で確実に撲滅できる。
② 個別手動インストールの悲劇 vs Composeコンテナ化(パラダイムシフト)
まずは、なぜローカルPCへのSQL Server個別インストールをやめ、コンテナ化へ舵を切るべきなのか、その劇的なパラダイムシフトを図解で確認しましょう。
従来の開発スタイルでは、開発者のPCそれぞれにギガバイト級のSQL Serverインストーラを実行し、ローカルのWindowsサービスとして常駐させていました。
これには以下のような致命的な問題が常に付きまとっていました。
- PCリソースの常時圧迫: PC起動時からバックグラウンドでSQL Serverサービスがメモリを数GB消費し、バッテリー駆動時間や動作速度を著しく低下させる。
- 環境差異(環境ドリフト)の発生: 「田中さんはSQL Server 2019、佐藤さんは2022」「インスタンス名が SQLEXPRESS と MSSQLSERVER で混在」「Collation(Japanese_CI_AS vs Japanese_CS_AS)が異なり文字列検索の挙動が乖離」。
- アンインストールと環境汚染: プロジェクト終了後、レジストリや共有コンポーネントがPC内に散乱し、完全にクリーンな状態へ戻すのが極めて困難。
一方、Docker Composeによるコンテナ運用では、SQL Serverはただの「隔離された軽量Linuxプロセス」にすぎません。
docker compose up -d を叩けば数秒で起動し、仕事が終われば docker compose down でPC上のリソースを完全にゼロへ戻せます。
もちろん、イメージタグ(例: 2022-latest)によりチーム全員が1ビットの狂いもなく完全同一のSQL Server環境を共有できます!
③ docker-compose.yml の2大サービス完全定義(SQL Server & Web API)
それでは、C# Web API と SQL Server を連携させる完全な docker-compose.yml の中身を見ていきましょう。
services:
db:
image: mcr.microsoft.com/mssql/server:2022-latest
container_name: my-sqlserver
environment:
- ACCEPT_EULA=Y # マイクロソフトのライセンス条項に同意
- MSSQL_SA_PASSWORD=YourStrong@Pass123 # SAパスワード(強度要件必須)
ports:
- "1433:1433" # ホストPCのSSMSやAzure Data Studioからの接続用
volumes:
- sqldata:/var/opt/mssql # DBファイルをボリュームで永続化!
healthcheck:
test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P 'YourStrong@Pass123' -C -Q 'SELECT 1' || exit 1"]
interval: 10s
timeout: 5s
retries: 5
start_period: 15s
webapi:
build:
context: .
dockerfile: Dockerfile
container_name: my-csharp-webapi
ports:
- "5000:8080" # ホストPCのブラウザから http://localhost:5000 でアクセス
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ConnectionStrings__DefaultConnection=Server=db,1433;Database=CatalogDb;User Id=sa;Password=YourStrong@Pass123;TrustServerCertificate=True;MultipleActiveResultSets=true;
depends_on:
db:
condition: service_healthy # DBがクエリ受付可能になるまで待機!
volumes:
sqldata: # Docker管理の名前付きボリューム
④ コンテナ間通信と内部DNS:EF Core接続文字列の正しい書き方
Docker Composeで初心者が最も躓くのが「接続文字列のサーバー名(ホスト名)」の指定です。
⚠️ なぜ Server=localhost では絶対に繋がらないのか?
「ローカルPC上で動かしているのだから、接続文字列は Server=localhost でいいのでは?」と考えてしまう方が非常に多いです。
しかし、コンテナ内部における localhost とは「そのコンテナ自身の隔離ネットワーク」を意味します。
webapi コンテナの中から localhost:1433 を探しても、そこにSQL Serverは存在しないため、即座に SqlException: A network-related or instance-specific error occurred がスローされて起動クラッシュします。
✅ 正解は Compose サービス名「Server=db,1433」
Docker Composeは、起動時に全コンテナが参加する専用の仮想ネットワーク(デフォルトブリッジネットワーク)を自動生成します。
そして、Compose内部に組み込まれたDNSサーバーが、サービス名(db や webapi)を該当コンテナのIPアドレスへと自動的に名前解決してくれます!
したがって、C#の接続文字列には以下のようにサービス名を指定するのが唯一絶対の正解です。
また、ASP.NET Coreの構成システムは、環境変数名における「二重アンダースコア(__)」を appsettings.json のコロン(:)区切り階層とみなす仕様があります。
Compose側で ConnectionStrings__DefaultConnection=... と設定するだけで、appsettings.json 内の値を自動的にオーバーライドできるため、Git管理するソースコードにパスワードをハードコードする必要もありません!
⑤ 単純なdepends_onの落とし穴と healthcheck による起動順序制御
「depends_on: - db と書いているのに、なぜかWeb APIが起動時にDB接続エラーで死んでしまう…」という現象に遭遇したことはありませんか?
その原因と解決策を図解で整理しましょう。
「プロセス起動」と「クエリ受付準備完了(Ready)」の決定的なタイムラグ
Dockerの単純な depends_on は、「前段のコンテナプロセス(PID)が立ち上がったこと」しか検知しません。
しかし、SQL Serverのような本格的RDBMSは、OSプロセスが起動してから内部で master データベースの復旧、トランザクションログのスキャン、TCPリスナーの開放を行うまでに、通常10秒〜20秒程度の初期化タイムラグが発生します。
プロセスが動いた直後にWeb APIが起動して接続しにいくと、リスナーがまだ開いていないため接続拒否(Connection Refused)で即死してしまうのです。
healthcheck + condition: service_healthy で完全同期
これを解決するのが、SQL Serverの稼働状態を内部からポーリング監視する healthcheck です。
SQL Serverコンテナ内にプリインストールされている sqlcmd ツールを使い、SELECT 1 クエリが正常に応答する(終了コード0を返す)かテストします。
interval: 10s: 10秒ごとにヘルスチェックを実行timeout: 5s: 応答が5秒以上無ければ失敗と判定retries: 5: 5回連続で成功/失敗を判定start_period: 15s: コンテナ起動直後の15秒間は失敗してもカウントしない
そして、Web API側の depends_on に condition: service_healthy を指定することで、SQL Serverがクエリを正常処理できる状態になるまで、Web APIの起動を安全に待機させることができます!
⑥ 実践ハンズオン:C# EF Coreの自動マイグレーション実装
Web APIとSQL Serverが安全に起動したら、初回起動時に自動的にデータベースとテーブルを作成する「EF Core 自動マイグレーション」を組み込みましょう。
Program.cs に以下のコードを追加します。
var builder = WebApplication.CreateBuilder(args);
// DbContextの登録(SQL Serverプロバイダー)
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddControllers();
var app = builder.Build();
// 起動時に自動で未適用のマイグレーションをSQL Serverへ流し込む
using (var scope = app.Services.CreateScope())
{
var services = scope.ServiceProvider;
var logger = services.GetRequiredService<ILogger<Program>>();
try
{
var dbContext = services.GetRequiredService<AppDbContext>();
logger.LogInformation("SQL Serverへのマイグレーション適用中...");
await dbContext.Database.MigrateAsync();
logger.LogInformation("データベースマイグレーション完了!");
}
catch (Exception ex)
{
logger.LogError(ex, "マイグレーション実行中にエラーが発生しました。");
throw;
}
}
app.MapControllers();
app.Run();
これで準備は完了です!ターミナルで以下のコマンドを実行してみましょう。
$ docker compose up -d --build
# ログをリアルタイムで追尾確認
$ docker compose logs -f
SQL Serverコンテナが立ち上がり、ヘルスチェックがhealthyになった瞬間にWeb APIコンテナが起動し、自動的に CatalogDb データベースとテーブル群が生成されます!
ホストPCのブラウザから http://localhost:5000/api/catalog にアクセスすれば、コンテナ内のSQL Serverから取得したJSONデータが即座に返ってきます。
⑦ YouTube動画連動:霊夢と魔理沙の掛け合い解説
本講義の内容は、YouTubeの動画講義(ゆっくり解説)でも楽しくビジュアルに学べます!
手動インストールの悲哀とComposeの快感を、霊夢と魔理沙の会話で振り返ってみましょう。
Server=db,1433 ってサービス名を書くだけで、Composeが勝手に内部DNSで繋いでくれるのがすごく便利ね!localhostって書いちゃダメな理由もよくわかったわ!」
⑧ 実務でありがちな落とし穴&ベストプラクティス
💡 現場で必ず遭遇する Compose 運用の4大落とし穴
落とし穴① SA_PASSWORDの強度ポリシー違反でコンテナが即死する
SQL Serverコンテナは、デフォルトで厳格なパスワードポリシーが有効になっています。
「最低8文字以上」かつ「英大文字・英小文字・数字・特殊記号(@, #, $など)」のうち3種類以上を含む必要があります。単純な password123 などにすると、起動直後に内部エラーでコンテナが強制終了します。
落とし穴② ホストPC側の 1433 ポートが衝突する
過去にホストPCに直接SQL Serverをインストールしたことがある場合、すでにローカルの 1433 ポートが使用されており、bind: address already in use で起動に失敗することがあります。
その場合は、ports: ["11433:1433"] のようにホスト側の受付ポートを 11433 等に変更してください。(コンテナ同士は db:1433 で通信するため、Web API側の接続文字列の変更は不要です)
落とし穴③ TrustServerCertificate=True の指定漏れによるSSL/TLSエラー
.NET 7 / 8 や Microsoft.Data.SqlClient の最新版では、SQL Serverとの通信暗号化がデフォルトで必須(Encrypt=True)となっています。
コンテナ内の自己署名証明書を受け入れるため、接続文字列に必ず TrustServerCertificate=True; を付与してください。
落とし穴④ volumes による永続化を忘れてデータが消滅する
volumes: - sqldata:/var/opt/mssql の記述を忘れると、コンテナを docker compose down して再作成した際、内部のmdf/ldfファイルがコンテナと一緒に破棄されてしまいます。開発用テストデータを保護するため、必ず名前付きボリュームを定義しましょう。
- SQL Serverのコンテナ化: 開発PCへの重厚な個別インストールを廃止し、公式Linuxイメージ(
mssql/server)で環境汚染ゼロのイミュータブル運用を実現する。 - 名前付きボリュームによる永続化:
sqldata:/var/opt/mssqlを定義し、コンテナを破棄・再作成してもDBデータを安全に維持する。 - 内部DNS名前解決: コンテナ間通信では
localhostではなくComposeサービス名(Server=db,1433)を接続文字列に指定する。 - healthcheck による確実な起動制御:
sqlcmdによるクエリ応答確認とcondition: service_healthyを組み合わせ、初期化前の接続クラッシュを完全撲滅する。
『Redisキャッシュを追加した3層コンテナアーキテクチャ』〜Web API + SQL Server + Redis の分散キャッシュ・セッション管理をComposeで構築〜
コメント
コメントを投稿