FastAPI 教學:在 Production 環境關閉 /docs 與 /redoc(OpenAPI 文件)
簡介
在開發階段,FastAPI 內建的 Swagger UI(/docs)與 ReDoc(/redoc)讓 API 使用者能即時檢視與測試介面,極大提升開發效率與除錯便利性。
然而,當應用程式部署到 Production 環境時,這兩個自動產生的文件頁面往往不該對外開放:
- 資訊安全 – 文件中會揭露所有路由、請求參數、回傳結構,攻擊者可藉此搜尋弱點。
- 效能考量 – 產生 OpenAPI schema 需要額外的計算與 I/O,對高流量服務會產生不必要的開銷。
- 品牌與使用者體驗 – 真正的前端或 API 客戶端通常會有自訂的文件或 API Portal,內建的 UI 可能不符合企業形象。
本篇文章將說明 為什麼、什麼時候以及如何在 Production 中安全、乾淨地關閉 /docs 與 /redoc,並提供多種實作範例與最佳實踐,幫助你在部署 FastAPI 時保持彈性與安全。
核心概念
1. FastAPI 產生文件的機制
FastAPI 會在應用程式啟動時自動建立一個 OpenAPI schema(openapi.json),並根據設定掛載兩個路由:
| 路由 | 預設 UI | 產生的檔案 |
|---|---|---|
/docs |
Swagger UI | swagger-ui.html |
/redoc |
ReDoc | redoc.html |
這兩個路由本質上是 普通的 FastAPI 路由,因此可以使用標準的路由註冊與移除方式加以管理。
2. 何時關閉文件
- 正式上線(Production):所有外部使用者僅能透過正式的 API 文件或客戶端存取。
- 測試環境(Staging):若測試環境與 Production 完全相同,建議同樣關閉,或使用環境變數做細部控制。
- 內部開發環境:保留
/docs與/redoc以利開發與除錯。
3. 使用環境變數控制
最常見的做法是 透過環境變數(如 ENV=production)在程式啟動時決定是否掛載文件路由。這樣可以在同一套程式碼中同時支援多種部署模式。
程式碼範例
以下示範 五種 常見且實用的關閉方式,均以 Python(FastAPI)為例,並附上完整註解。
範例 1️⃣:最簡單的 docs_url=None / redoc_url=None
在建立 FastAPI 實例時直接傳入 None,即可完全不產生對應路由。
# main.py
from fastapi import FastAPI
# 在 Production 中直接關閉文件
app = FastAPI(
title="My Production API",
docs_url=None, # 關閉 /docs (Swagger UI)
redoc_url=None, # 關閉 /redoc (ReDoc)
openapi_url="/openapi.json" # 若仍需要 OpenAPI JSON,可自行保留
)
@app.get("/items/{item_id}")
async def read_item(item_id: int):
"""示範端點"""
return {"item_id": item_id}
重點:此方式在程式啟動即決定,無法在不同環境動態切換。適合「只在 Production」的單一部署情境。
範例 2️⃣:利用環境變數動態設定
透過 os.getenv 判斷當前環境,決定是否保留文件路由。
# main.py
import os
from fastapi import FastAPI
ENV = os.getenv("ENV", "development") # 預設為 development
if ENV == "production":
# Production:關閉文件
docs_url = None
redoc_url = None
else:
# Development / Staging:保留文件
docs_url = "/docs"
redoc_url = "/redoc"
app = FastAPI(
title="Dynamic Docs API",
docs_url=docs_url,
redoc_url=redoc_url,
openapi_url="/openapi.json"
)
@app.get("/ping")
async def ping():
return {"msg": "pong"}
技巧:將環境變數寫入 Docker Compose、Kubernetes ConfigMap 或 CI/CD pipeline 中,保持部署與程式碼的分離。
範例 3️⃣:在 startup 事件中手動移除路由
如果你已在 FastAPI() 時啟用了文件,但想在 startup 階段根據條件移除它們。
# main.py
import os
from fastapi import FastAPI
app = FastAPI()
@app.get("/hello")
async def hello():
return {"msg": "Hello World"}
@app.on_event("startup")
async def disable_docs():
"""根據環境在啟動時移除 /docs 與 /redoc"""
if os.getenv("ENV") == "production":
# 移除 swagger UI
app.router.routes = [
route for route in app.router.routes
if route.path not in ("/docs", "/redoc")
]
# 同時關閉 OpenAPI schema(若不需要)
app.openapi_url = None
說明:此方法不需要在建立
FastAPI時傳入None,可在同一程式碼庫中保留完整文件,僅在需要時「動態」關閉。
範例 4️⃣:自訂 OpenAPI 產生函式,僅在非 Production 時回傳 schema
如果你仍想保留 /docs 介面但不希望外部取得 openapi.json,可以自訂 openapi 方法。
# main.py
import os
from fastapi import FastAPI
from fastapi.openapi.utils import get_openapi
app = FastAPI()
def custom_openapi():
"""只在非 production 環境產生 OpenAPI schema"""
if os.getenv("ENV") == "production":
return None # 直接回傳 None,Swagger UI 會顯示無法載入
if app.openapi_schema:
return app.openapi_schema
openapi_schema = get_openapi(
title="Conditional OpenAPI",
version="1.0.0",
routes=app.routes,
)
app.openapi_schema = openapi_schema
return app.openapi_schema
app.openapi = custom_openapi
@app.get("/users/{user_id}")
async def get_user(user_id: int):
return {"user_id": user_id}
優點:
/docs仍可在開發環境正常使用,而 Production 中會自動顯示「OpenAPI schema not found」的訊息,避免資訊外洩。
範例 5️⃣:使用中介層(Middleware)攔截文件請求
若你想在 所有環境 保留路由,但僅對特定 IP 或認證通過者開放,可透過 Middleware 進行授權控制。
# main.py
from fastapi import FastAPI, Request, HTTPException
from starlette.status import HTTP_403_FORBIDDEN
app = FastAPI()
ALLOWED_IPS = {"127.0.0.1"} # 僅允許本機存取文件
@app.middleware("http")
async def block_docs(request: Request, call_next):
path = request.url.path
if path in ("/docs", "/redoc", "/openapi.json"):
client_ip = request.client.host
if client_ip not in ALLOWED_IPS:
raise HTTPException(status_code=HTTP_403_FORBIDDEN,
detail="Access to API docs is forbidden")
response = await call_next(request)
return response
@app.get("/status")
async def status():
return {"status": "ok"}
應用:在內部測試環境僅允許公司內部 IP 存取
/docs,外部使用者則會得到 403 Forbidden。
常見陷阱與最佳實踐
| 常見問題 | 為什麼會發生 | 解決方案或最佳做法 |
|---|---|---|
忘記在 Production 中關閉 /docs |
多數開發者只在本地測試時注意文件,部署腳本未加以檢查。 | 在 CI/CD pipeline 加入 環境變數檢查,或使用 Dockerfile 中的 ENV 變數強制設定。 |
關閉 /docs 後,Swagger UI 仍能讀取 openapi.json |
只關閉 UI,卻忘記同時將 openapi_url 設為 None。 |
同時設定 docs_url=None, redoc_url=None, openapi_url=None,或自訂 app.openapi = lambda: None。 |
使用 docs_url=None,但仍在程式碼中 app.include_router() 時產生警告 |
某些第三方套件會在啟動時自動掛載文件路由。 | 在 include_router 時使用 include_in_schema=False,或在套件的設定檔中關閉自動文件。 |
| 中介層(Middleware)攔截失敗,仍回傳文件 | 中介層寫在錯誤位置或未正確返回 call_next。 |
確認 Middleware 在 FastAPI() 建立後、路由註冊前 加入,且 raise HTTPException 正確拋出。 |
| 開發人員忘記切換環境變數,導致 Production 暴露文件 | 部署腳本與程式碼分離不佳。 | 使用 .env 檔案或 Kubernetes Secret,並在程式碼中明確 assert os.getenv("ENV") 以防止預設為 development。 |
建議的最佳實踐
- 環境變數化:在
Dockerfile、docker-compose.yml、或 CI/CD 中明確設定ENV=production,並在程式碼裡僅根據此變數決定文件路由。 - 最小化公開資訊:在 Production 中同時關閉
openapi_url,即使有人猜到/docs位置也無法取得 schema。 - 自動化測試:在測試階段加入 安全檢查,確保
/docs、/redoc、/openapi.json在 Production 環境返回 404 或 403。 - 文件管理分離:若公司需要正式的 API 文件,建議使用 SwaggerHub、Redocly、或自建的 API Portal,而非 FastAPI 內建 UI。
- 日誌與監控:在關閉文件後,仍可在日誌中記錄對
/docs、/redoc的請求,以偵測潛在的掃描或攻擊行為。
實際應用場景
| 場景 | 為什麼要關閉文件 | 實作方式 |
|---|---|---|
| 金融業線上交易平台 | 法規要求隱藏所有 API 結構,避免資安風險。 | 於 Dockerfile 中設定 ENV=production,使用 範例 2 的環境變數控制,並將 openapi_url=None。 |
| 大型電商的微服務 | 每個微服務都有內部測試文件,但外部只提供統一的 API Portal。 | 在每個服務的 startup 事件使用 範例 3 直接移除 /docs 與 /redoc,同時保留 openapi.json 供內部 CI 使用。 |
| 內部工具平台(僅公司員工使用) | 需要文件給開發者,但不希望公開給外部網路。 | 使用 範例 5 的 Middleware,僅允許公司內部 IP 存取文件。 |
| 多租戶 SaaS | 每個租戶都有自己的子域名,文件只允許租戶管理員查看。 | 結合 OAuth2 或 API Key,在 Middleware 中驗證權限後才返回文件,否則回傳 403。 |
| 快速原型驗證 | 開發階段需要即時測試,部署後立即關閉。 | 在 CI pipeline 完成部署後,執行 kubectl set env deployment/myapi ENV=production,觸發 範例 2 的自動切換。 |
總結
- 關閉
/docs與/redoc是 Production 環境的安全基本功,能有效降低資訊外洩與不必要的效能負擔。 - FastAPI 提供了 多種彈性方式(在實例化時直接設定、環境變數控制、啟動事件移除、OpenAPI 自訂、Middleware 攔截)讓開發者依需求選擇最合適的方案。
- 最佳實踐:使用環境變數統一管理、同時關閉
openapi_url、加入自動化安全測試與日誌監控,確保文件在 Production 完全不可被未授權的使用者取得。 - 在實務上,根據 業務需求(金融、電商、內部工具)與 部署管線(Docker、K8s、CI/CD)選擇適當的實作方式,可讓你的 FastAPI 應用在保護資訊安全的同時,仍保持開發效率與部署彈性。
祝你在 FastAPI 的開發與部署旅程中,既能快速迭代,又能安全無憂! 🚀