🐳

【実測】C#エンジニアがDockerを学び、マルチステージビルドで379MBまで軽量化

に公開

📝 C# TodoアプリのDocker化

Dockerの本を一冊読み基礎的なことがさらっと分かったので、主に業務で使っているC#を用いて Docker環境を作ることにしました。

本件が完了次第Goでも同様のアプリを作り、どのような差が出るのか調査していく予定です。

🙋🏽‍♂️ 私の簡単なプロフィール

  • エンジニア歴4ヶ月目(事前に約半年自己学習)
  • 業務で使用している言語はC#
  • バックエンドが好き

☝️ 実践しようと思った背景

  • Dockerの実用化・知識定着目的
  • この後GoでもTodoアプリのDocker化を行うが、その差を見たかった

🎯 この記事で学べること

  • ASP.NET Core Razor Pagesアプリの作成から Docker化まで
  • マルチステージビルドによるイメージ最適化
  • 実際のパフォーマンス測定結果

🏗️ 1. プロジェクト作成から動作確認まで

測定環境詳細

  • MacBook Air (Apple Silicon M3)
  • メモリ: 16GB
  • Docker Desktop: 4.23.0
  • .NET: 8.0.411
  • 測定日時: 2025年7月1日

環境準備

# 必要な環境
- .NET 8.0 SDK
- Docker Desktop

# プロジェクト作成
mkdir docker-learning-todo-comparison
cd docker-learning-todo-comparison
mkdir csharp-todo
cd csharp-todo

dotnet new webapi -n SimpleTodoAPI
cd SimpleTodoAPI

必要なパッケージ追加

dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.InMemory
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL

実装した機能

  • CRUD操作: Todo作成・一覧・編集・削除
  • Razor Pages: サーバーサイドレンダリング
  • Bootstrap UI: レスポンシブデザイン
  • Entity Framework: In-Memoryデータベース
  • バリデーション: フォーム入力検証

🐳 2. Docker化の実装

マルチステージDockerfile

# Build stage - SDK使用(大きいが開発ツール完備)
# コンパイラ、MSBuild、NuGet、デバッガーなどを含む
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build

# コンテナ内の作業フォルダを/appに設定
WORKDIR /app

# 依存関係を先に復元(キャッシュ最適化)
# .csprojは変更頻度が低いため、先に処理をしておく。
COPY *.csproj ./

# restoreによってNuGetパッケージのダウンロードと復元を行う
RUN dotnet restore

# ソースコードをコピーしてビルド
COPY . ./
RUN dotnet publish -c Release -o out

# Runtime stage - 軽量なランタイムのみ
# SDKと違い、コンパイラ、MSBuild、SDKツール群は含まれない
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
COPY --from=build /app/out .

# セキュリティ: 非rootユーザーで実行
# 非rootユーザーで実行することでコンテナ侵害時のリスクを低減
RUN groupadd -r appuser && useradd -r -g appuser appuser
RUN chown -R appuser:appuser /app

# これ以降appuserとして実行される
USER appuser

EXPOSE 8080
ENV ASPNETCORE_URLS=http://+:8080

ENTRYPOINT ["dotnet", "SimpleTodoAPI.dll"]

最適化のポイント

  1. マルチステージビルド: SDK(700MB) → Runtime(200MB)へ軽量化
  2. レイヤーキャッシュ: 依存関係とソースコードを分離して高速化
  3. セキュリティ: 非rootユーザーでの実行
  4. .dockerignore: 不要ファイルの除外

📊 3. パフォーマンス測定

Docker Build & Run

# イメージビルド
docker build -t csharp-todo-app .
# ✅ 成功: マルチステージビルド完了

$ docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"
REPOSITORY                              TAG     SIZE
csharp-todo-app                        latest  379MB
mcr.microsoft.com/dotnet/aspnet        8.0     216MB  # ベースイメージ
mcr.microsoft.com/dotnet/sdk           8.0     742MB  # ビルド用(最終には不含)

# サイズ削減効果の計算
シングルステージ想定: 742MB (SDK + アプリ)
マルチステージ実測: 379MB
削減効果: 363MB (48.9%の軽量化)
追加サイズ: 163MB (アプリケーション + 依存関係)

# 複数回測定による平均値算出
$ for i in {1..5}; do
    docker rm -f todo-container 2>/dev/null
    time docker run -d -p 8080:8080 --name todo-container csharp-todo-app
  done

測定結果:
1回目: 0.305s
2回目: 0.298s  
3回目: 0.312s
4回目: 0.301s
5回目: 0.295s

平均起動時間: 0.302s (標準偏差: 0.007s)

# アプリが実際にHTTPリクエストに応答するまでの時間
$ docker run -d -p 8080:8080 --name todo-container csharp-todo-app

# 起動直後から1秒おきに応答チェック
$ for i in {1..10}; do
    echo -n "$(date '+%T'): "
    curl -s -o /dev/null -w "%{http_code} - %{time_total}s\n" http://localhost:8080 || echo "接続失敗"
    sleep 1
  done

実測結果:
06:45:01: 000 - 0.000s (接続失敗)
06:45:02: 200 - 0.245s 初回応答成功
06:45:03: 200 - 0.012s
06:45:04: 200 - 0.008s
06:45:05: 200 - 0.007s

# 結論
コンテナ起動: 0.3秒
アプリ初回応答: 起動から1-2秒後
安定応答: 0.007-0.012秒

# 30秒間隔で10分間監視
$ for i in {1..20}; do
    echo "$(date '+%T'): $(docker stats todo-container --no-stream --format 'MEM: {{.MemUsage}} CPU: {{.CPUPerc}}')"
    sleep 30
  done

実測データ:
06:50:00: MEM: 45.2MiB / 7.654GiB CPU: 0.8%   # 起動直後
06:50:30: MEM: 51.84MiB / 7.654GiB CPU: 0.1%  # 安定状態
06:51:00: MEM: 51.92MiB / 7.654GiB CPU: 0.0%
06:51:30: MEM: 51.88MiB / 7.654GiB CPU: 0.0%
...
07:00:00: MEM: 52.1MiB / 7.654GiB CPU: 0.01%  # 10分後

# 分析
起動時メモリスパイク: 45.2MiB
安定時メモリ使用量: 51.8-52.1MiB (約52MiB)
メモリ増加: 6.8MiB (15%増加後安定)

# Apache Benchによる負荷テスト
$ ab -n 1000 -c 10 http://localhost:8080/

Server Software:        Kestrel
Server Hostname:        localhost
Server Port:            8080

Document Path:          /
Document Length:        802 bytes

Concurrency Level:      10
Time taken for tests:   2.456 seconds
Complete requests:      1000
Failed requests:        0
Total transferred:      1045000 bytes
HTML transferred:       802000 bytes
Requests per second:    407.14 [#/sec] (mean)
Time per request:       24.56 [ms] (mean)
Time per request:       2.456 [ms] (mean, across all concurrent requests)
Transfer rate:          415.54 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    1   0.8      1       5
Processing:    12   23   5.2     22      45
Waiting:       11   22   5.1     21      44
Total:         13   24   5.3     23      46

# 負荷テスト中のリソース使用量
$ docker stats todo-container --no-stream
CONTAINER ID   NAME             CPU %     MEM USAGE / LIMIT     MEM %     NET I/O           BLOCK I/O    PIDS
dbff2abd1ef8   todo-container   45.2%     67.3MiB / 7.654GiB   0.86%     89.1kB / 1.02MB   0B / 0B      25

# 負荷テスト後(安定状態復帰)
$ docker stats todo-container --no-stream
CONTAINER ID   NAME             CPU %     MEM USAGE / LIMIT     MEM %     NET I/O           BLOCK I/O    PIDS
dbff2abd1ef8   todo-container   0.02%     54.1MiB / 7.654GiB   0.69%     89.1kB / 1.02MB   0B / 0B      21

実測パフォーマンス

項目 結果 備考
イメージサイズ 379MB マルチステージで軽量化済み
平均起動時間 0.302秒 Docker run コマンド実行時間
初回応答 1-2秒 起動から2秒後のHTTPレスポンス
メモリ使用量 52MiB 待機時のメモリ消費
負荷時レスポンス 24.56ms Apache Benchによる負荷テスト
スループット 407 req/s 1秒間のリクエスト処理量

💡 4. 学んだポイント

技術的な学習

# マルチステージビルドの効果
従来の方法: 700MB (SDK + アプリ)
今回の方法: 379MB (ランタイム + アプリ) 
 46%の軽量化達成

開発効率の向上

# レイヤーキャッシュによる高速化
初回ビルド: 5分程度
2回目以降: 30秒程度(ソースコード変更時)
 90%の時間短縮

運用面でのメリット

  • 軽量: クラウドでの転送時間短縮
  • セキュア: 非rootユーザーでの実行
  • ポータブル: 環境差異の解決

🔍 5. トラブルシューティング実例

よくあるエラーと解決法

問題1: Scriptsセクションエラー

InvalidOperationException: The following sections have been defined but have not been rendered

解決: _Layout.cshtml@await RenderSectionAsync("Scripts", required: false)を追加

問題2: 400エラー(フォーム送信時)

解決: モデルの初期化とアンチフォージェリートークンの設定

問題3: Todoが表示されない

解決: Program.csでのRazor Pages設定とTodoContext登録の確認


🎯 6. 次のステップ

今回の成果物

  • ✅ 完全に動作するC# Todoアプリ
  • ✅ 本格的?なDockerfile(マルチステージビルド)
  • ✅ 詳細なパフォーマンスデータ

次の挑戦

  • Go版の実装: 同じ機能をGoで実装
  • パフォーマンス比較: C# vs Go の詳細分析
  • docker-compose: 複数サービス連携
  • 本格的な監視: メトリクス・ログ収集

📚 参考リンク


💬 まとめ

今回のプロジェクトで、理論だけでなく実際に手を動かしてDockerを学ぶことができました。特に:

  • マルチステージビルドの効果を数値で実感
  • 実際の開発ワークフローでのDocker活用
  • パフォーマンス測定の重要性

とはいえC#でしか実施していないので、これがどのくらい速いのか遅いのかよく分かっていません。

次回はGo版を実装して、言語の違いがコンテナ運用に与える影響を詳しく比較していきます。

DevOpsエンジニアを目指す皆さん、一緒に学んでいきましょう!


Discussion