🐳
【実測】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"]
最適化のポイント
- マルチステージビルド: SDK(700MB) → Runtime(200MB)へ軽量化
- レイヤーキャッシュ: 依存関係とソースコードを分離して高速化
- セキュリティ: 非rootユーザーでの実行
- .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: 複数サービス連携
- 本格的な監視: メトリクス・ログ収集
📚 参考リンク
- GitHub Repository: https://github.com/Some558/docker-learning-todo-comparison
- Docker Best Practices: https://docs.docker.com/develop/dev-best-practices/
- ASP.NET Core in Docker: https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/docker/
-
開発系エンジニアのためのDocker絵とき入門:
https://www.amazon.co.jp/開発系エンジニアのためのDocker絵とき入門-鈴木亮-ebook/dp/B0CSFF6QYD/ref=sr_1_8?__mk_ja_JP=カタカナ&crid=1W6YFYXXJN1BV&dib=eyJ2IjoiMSJ9.MyGFCwWV1O6Tf_XbfenDznh6Etu1iJHixqWr2ObRJE0TbgjivUlbmT78t6eOcLPtZwfWvM7Ftn1jsXTF8MWkYShHVs9kc5_5QCcTEiowYFQqPJXPqBNa_zoXEJMDOivSTOC4QHJ_fX7GvenoxLE8Px1qLw8n6XMMfO8LcJNOxlYxoUdFjWPyCw2IY8zM4WJzBaStQigZpMwi1GG8rna6SYhOiok-8C614qWVcR74H-KVGh10ECbL-Cof4QgEN_3sIqtl7haFxJy9BrXmSjLlcrZ181r0WA24aWnogA8Xvpc._0b37zWczwUUXqUBkYKo-bDCEudo2ZtyDVMzbO_r9uk&dib_tag=se&keywords=Docker&qid=1751972927&sprefix=docker%2Caps%2C929&sr=8-8
💬 まとめ
今回のプロジェクトで、理論だけでなく実際に手を動かしてDockerを学ぶことができました。特に:
- マルチステージビルドの効果を数値で実感
- 実際の開発ワークフローでのDocker活用
- パフォーマンス測定の重要性
とはいえC#でしか実施していないので、これがどのくらい速いのか遅いのかよく分かっていません。
次回はGo版を実装して、言語の違いがコンテナ運用に与える影響を詳しく比較していきます。
DevOpsエンジニアを目指す皆さん、一緒に学んでいきましょう!
Discussion