ローカルの Visual Studio では Swagger UI が見えるのに、本番サーバー(EC2/IIS/Nginx/ALB など)では /swagger/index.html が 404——。この落差は「コードの不具合」ではなく、ASP.NET Core の既定動作と配備構成の差で起きることがほとんどです。この記事では、最短の修正から構成別の対処、セキュリティ強化、診断コマンドまでを一気通貫で解説します。
現象の再整理(よくある訴求)
- ローカル(
https://localhost:7173/swagger/index.htmlなど)では表示される。 - EC2/Linux や Windows Server に配備すると
https://<ドメイン>/swagger/index.htmlが 404。 - 以前は本番でも見えたという話があるが、現在は設定が不明。
- サーバー側で Swagger UI を起動・閲覧したい。
結論(最短の修正)
ASP.NET Core のテンプレートでは Swagger ミドルウェアが 開発環境限定で有効化されています。Production でも常時有効にするなら、Program.cs で環境条件分岐を外してください。
// 既定のテンプレート(開発時のみ)
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
// → 本番でも常時有効にする場合(推奨は後述の制限付き公開)
app.UseSwagger();
app.UseSwaggerUI(); // 必要に応じてオプションを設定
前提として Program.cs に次が含まれている必要があります(テンプレート既定)。
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
再ビルド・再デプロイ後、/swagger/index.html にアクセスできるようになります。
なぜ本番だけ表示されないのか(仕組みの要点)
- テンプレートの既定は「開発時のみ Swagger を出す」。本番では API 仕様の露出を避けるために無効。
- 本番サーバーの
ASPNETCORE_ENVIRONMENTは通常Production。条件IsDevelopment()に合致しないため Swagger ミドルウェアが配線されない。 - さらに、リバースプロキシ(Nginx/IIS/ALB)で パスが書き換わる と、
/swaggerへのルーティングが失敗して 404 になります。
5分で終わる原因切り分けチェックリスト
| 観点 | 確認コマンド / 方法 | 期待/判定 | 対処の方向性 |
|---|---|---|---|
| 環境モード | printenv ASPNETCORE_ENVIRONMENT(Linux)/ システム環境変数(Windows) | Production ならテンプレ既定で無効 | UseSwagger() を常時有効 or 制限付き公開に変更 |
| Swagger JSON | curl -I https://<ドメイン>/swagger/v1/swagger.json | 200 が返る | 返らない場合はミドルウェア未配線 or パスベース不整合 |
| パスベース | 配備 URL が /api 等のサブパスか | アプリの実パスと一致 | UsePathBase() と RoutePrefix/SwaggerEndpoint を整合 |
| HTTPS 判定 | ALB/Nginx 経由で https → http の場合 | UI 内の JSON パスが https/相対パスで解決 | UseForwardedHeaders() と相対パス指定を徹底 |
| リバースプロキシ | Nginx/IIS のリライト設定 | /swagger/ が書き換えられない | 先頭で除外ロケーション/ハンドラを追加 |
| パッケージ | プロジェクトの参照 | Swashbuckle.AspNetCore が参照済み | 不足なら復旧(テンプレなら既定で入る) |
最短のコード例(.NET 8 / Minimal Hosting)
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// 逆プロキシ越しの HTTPS 判定を正しくする(ALB/Nginx 配備時は強く推奨)
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedFor
});
app.UseSwagger();
app.UseSwaggerUI(c =>
{
// 重要:相対パスで指定(プロキシ配備でも崩れにくい)
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API v1");
// 必要なら UI の公開パスを変更
// c.RoutePrefix = "docs"; // => /docs で公開
});
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
Production での公開は「制限付き」を強く推奨
本番で常時公開する場合は、最低限の保護をかけましょう。以下は Swagger パスだけ を簡易 Basic 認証で守る例です(実運用は IP 制限や OIDC による認証を推奨)。
bool IsSwaggerRequest(PathString path) => path.StartsWithSegments("/swagger");
app.UseWhen(ctx => IsSwaggerRequest(ctx.Request.Path), branch =>
{
branch.Use(async (ctx, next) =>
{
string auth = ctx.Request.Headers.Authorization;
if (string.IsNullOrEmpty(auth) || !auth.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase))
{
ctx.Response.Headers["WWW-Authenticate"] = "Basic realm="Swagger"";
ctx.Response.StatusCode = StatusCodes.Status401Unauthorized;
return;
}
// ユーザー名:パスワード(例:swagger:secret)
var expected = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("swagger:secret"));
var token = auth.Substring("Basic ".Length).Trim();
if (!string.Equals(token, expected, StringComparison.Ordinal))
{
ctx.Response.StatusCode = StatusCodes.Status403Forbidden;
return;
}
await next();
});
});
app.UseSwagger();
app.UseSwaggerUI();
配備構成別の落とし穴と対処
EC2(Amazon Linux)+ Nginx + Kestrel
典型的な構成では、Nginx が 443 を受けて Kestrel にプロキシします。location / のリライトが強すぎると /swagger/ が SPA の index.html に吸い込まれて 404 風になります。先頭で除外するか、個別ロケーションを追加しましょう。
server {
listen 443 ssl;
server_name api.example.com;
```
# 省略: SSL 設定
# Swagger を最優先で素通し
location /swagger/ {
proxy_pass http://127.0.0.1:5000/swagger/;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# API 全体をプロキシ
location / {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
```
}
また、Program.cs 側では UseForwardedHeaders を有効にして https 判定を正しくし、Swagger UI 内のリンク崩れを防ぎます。
ALB(アプリケーションロードバランサ)でパスベース配備(例:/api)
ALB で /api/* をターゲットグループへ流していると、実アプリは /api を 基底パス として扱う必要があります。
app.UsePathBase("/api"); // 実行アプリの基底パスを /api に
app.UseSwagger();
app.UseSwaggerUI(c =>
{
// すべて相対パスで記述(/api をハードコードしない)
c.SwaggerEndpoint("/api/swagger/v1/swagger.json", "API v1");
c.RoutePrefix = "swagger"; // => /api/swagger
});
IIS(Windows Server)+ ASP.NET Core Module
Web.config などで環境変数を制御できます。Production でも一時的に開くなら、コードを変えず ASPNETCORE_ENVIRONMENT を Development に切替える手もあります(公開しっぱなしにしないこと)。
<configuration>
<system.webServer>
<handlers>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" ... />
</handlers>
<aspNetCore processPath="dotnet" arguments="MyApi.dll" stdoutLogEnabled="true" ...>
<environmentVariables>
<add name="ASPNETCORE_ENVIRONMENT" value="Production" />
</environmentVariables>
</aspNetCore>
</system.webServer>
</configuration>
URL 書き換え(URL Rewrite)や Application Request Routing (ARR) を使っている場合、/swagger/ を除外パターンに含めるのが安全です。
Apache httpd(mod_proxy)
<VirtualHost *:443>
ServerName api.example.com
# SSL 設定 省略
ProxyPreserveHost On
RequestHeader set X-Forwarded-Proto "https"
# Swagger を先に通す
ProxyPass /swagger/ [http://127.0.0.1:5000/swagger/](http://127.0.0.1:5000/swagger/)
ProxyPassReverse /swagger/ [http://127.0.0.1:5000/swagger/](http://127.0.0.1:5000/swagger/)
# 他はアプリへ
ProxyPass / [http://127.0.0.1:5000/](http://127.0.0.1:5000/)
ProxyPassReverse / [http://127.0.0.1:5000/](http://127.0.0.1:5000/)
エラー別トラブルシュート早見表
| 症状 | 主因 | 対処 |
|---|---|---|
| 404 /swagger/index.html | ミドルウェア未配線 / パス書き換え | UseSwagger() を条件外に出す。プロキシで /swagger/ を除外・優先。 |
| 404 /swagger/v1/swagger.json | Doc 名不一致 / バージョニングの設定漏れ | AddSwaggerGen() の SwaggerDoc("v1"...) と UI の SwaggerEndpoint を合わせる。 |
| UI のボタンを押すと http に飛ぶ | X-Forwarded-Proto 未設定 | プロキシでヘッダを付与、アプリで UseForwardedHeaders。 |
| リダイレクトループ | UseHttpsRedirection とプロキシ設定の不整合 | プロキシで https を明示、アプリ側はヘッダを信頼。 |
| 403/401 | 認証・認可でブロック | Swagger パスだけに緩和ルールまたは専用認証を適用。 |
環境・設定の把握(表で整理)
| 項目 | Linux (systemd) | Windows (IIS/サービス) | ポイント |
|---|---|---|---|
| 環境変数 | /etc/systemd/system/myapi.service の Environment=ASPNETCORE_ENVIRONMENT=Production | システム環境変数 or web.config | Development にするとテンプレ既定で Swagger が有効 |
| ポート | ASPNETCORE_URLS=http://127.0.0.1:5000 | IIS が受け持ち(OutOfProcess) | 直接 80/443 を Kestrel で聴かせない構成が安全 |
| ログ | journalctl -u myapi -n 200 | IIS の stdout ログ | UI が 404 のときもアプリ側ログを必ず確認 |
複数ドキュメント・複数環境への対応
API バージョンや環境ごとに OpenAPI を分けたいケースでは、SwaggerDoc を複数登録し、UI 側で切り替えます。
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new() { Title = "My API", Version = "v1" });
c.SwaggerDoc("v2", new() { Title = "My API", Version = "v2" });
});
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API v1");
c.SwaggerEndpoint("/swagger/v2/swagger.json", "My API v2");
});
サブパス配備(/myapp など)での確実な設定
アプリをサブディレクトリに配備するなら、UsePathBase と UI の RoutePrefix を整えます。相対パス指定が鍵です。
app.UsePathBase("/myapp");
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.RoutePrefix = "swagger"; // => /myapp/swagger
c.SwaggerEndpoint("/myapp/swagger/v1/swagger.json", "API v1");
});
設定をコードではなく構成で切り替える(おすすめ)
「一部の本番だけ許可したい」をコード分岐で書かず、構成値で切り替えるのが運用しやすいです。
{
"Swagger": {
"Enabled": false,
"RoutePrefix": "swagger"
}
}
var swaggerSection = builder.Configuration.GetSection("Swagger");
bool swaggerEnabled = swaggerSection.GetValue("Enabled", false);
string routePrefix = swaggerSection.GetValue("RoutePrefix", "swagger");
if (swaggerEnabled)
{
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.RoutePrefix = routePrefix;
c.SwaggerEndpoint($"/{routePrefix}/v1/swagger.json", "API v1");
});
}
環境ごとに構成ファイルや環境変数で Swagger:Enabled=true にすれば、コードを触らず公開・非公開を切り替えられます。
.NET 6 以前(Startup クラス)の例
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddEndpointsApiExplorer();
services.AddSwaggerGen();
}
```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// 本番含め常時
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "API v1");
});
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
}
```
}
セキュリティの勘所(公開リスクと対策)
- Swagger は 全エンドポイントとスキーマ を可視化します。攻撃者の踏み台情報にもなり得るため、外部公開時は保護必須。
- IP 制限:プロキシで社内/VPN のみ許可。
- 認証:OIDC/OAuth2(社内 IdP)や簡易 Basic 認証でガード。
- 別環境切替:Staging のみ有効化して参照用にする運用。
- ビルド分離:CI/CD のプロファイルごとに
Swagger:Enabledを制御。
診断のための定番コマンド集
# Linux: 環境変数とリッスン確認
printenv | grep ASPNETCORE
ss -lntp | grep 5000
# systemd: サービス定義/ログ
systemctl cat myapi.service
journalctl -u myapi -n 200
# 動作確認
curl -I https://<ドメイン>/swagger/index.html
curl -I https://<ドメイン>/swagger/v1/swagger.json
# 証明書/プロキシ経路の確認(詳細)
curl -vk https://<ドメイン>/swagger/
チェック済みデプロイ手順(安全第一の標準案)
- コード:
UseSwagger()とUseSwaggerUI()を常時配線。UseForwardedHeadersを有効化。 - 構成:
Swagger:Enabledを環境変数で制御。Production は基本 false。 - プロキシ:
/swagger/の除外ロケーション(Nginx/Apache/IIS)を追加。X-Forwarded-Protoを付与。 - 認可:Swagger パスに限定して認証/認可を適用(Basic/OIDC/IP 制限)。
- 検証:
/swagger/index.htmlと/swagger/v1/swagger.jsonをcurl -Iで確認。 - 監査:公開範囲(社外/社内)を明文化し、運用手順に組み込み。
よくある「それでも見えない」ケースと解決のコツ
- UI は 200 だがリソースが 404:UI が参照する
swagger-ui.cssなどはミドルウェアが埋め込みリソースとして配信します。プロキシが*.cssを別ハンドラに回していないか確認。 - API Versioning を導入後に 404:
SwaggerDoc("v1"...)と[ApiVersion("1.0")]の対応を見直し、DocInclusionPredicateを設定。 - SPA と共存:
UseSpa()やフロントのtry_files $uri /index.htmlが/swaggerを飲み込むことがある。除外を先頭に書く。 - コンテナ配備:
ASPNETCORE_URLSと公開ポートのマッピングを再点検。ヘルスチェックのパスを/swagger/v1/swagger.jsonにした場合、保護を忘れない。
まとめ
本番サーバーで Swagger UI が 404 になる最大要因は、テンプレ既定の「開発時のみ有効」とプロキシ配備のパス不整合です。UseSwagger()/UseSwaggerUI() を常時配線し、相対パス指定・UseForwardedHeaders・プロキシ側の /swagger/ 除外をセットで行えば、多くの環境で安定して表示できます。公開は最小限にとどめ、IP 制限や認証で保護するのが本番運用の定石です。ここまでの手順とチェックリストを踏めば、EC2/IIS/Nginx/ALB のどれでも短時間で復旧できます。
付録:テンプレの if ブロックを残したまま本番だけ開く(安全寄り)
if (app.Environment.IsDevelopment() || builder.Configuration.GetValue("Swagger:Enabled", false))
{
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "API v1"));
}
環境変数 Swagger__Enabled=true(ダブルアンダースコア)で一時公開できます。緊急時の切替に便利ですが、恒常公開は避けましょう。

コメント