# 部署

Source: https://gortexa.sheng.page/deployment/

框架 repo 的 `deploy/` 附了可直接參考的 Dockerfile（multi-stage、兩個 stage 都以 digest 釘住；直接用已提交的 `gen/` 建置而不在映像內重跑 proto 產生，runtime stage 是 `distroless/static:nonroot` 並以 `USER nonroot:nonroot` 執行）與 docker-compose（含 otelcol、Prometheus、Grafana、PgBouncer 設定）。以下三件事是框架刻意不代勞、部署者必須自己接的。

**上線前必接**
TLS 終結、authorization、readiness 依賴檢查這三項框架不代勞。任一項沒接，服務直接對公網時會靜默失去傳輸安全或授權——沒有錯誤、沒有警告。

## Scaffold 的 secret preflight

新 scaffold 會帶有本機開發用設定；在第一次 git add、Docker build 或交給 CI 前，先檢查 secret 是否已外部化。**不要把可用 JWT signing secret 留在版控或 image layer。**

- 將 real secret 交給 secret manager、mounted file 或 runtime environment，以 GORTEXA_AUTH__JWT_SECRET 注入。
- 讓 repo 只保留不可用的 placeholder 或不含 secret 的範例設定；local override 應被忽略。
- 讓 Docker build context 與 runtime image 都不含 local secret config，並驗證 image 只由 runtime injection 啟動。參考用的 `.dockerignore` 已把 `.git`、`.env`、`*.local.yaml` 擋在 build context 外——fork 時請保留。
- 規劃 rotation、issuer 與 audience；共享 secret 的服務應設定不同 audience。

這是 deployment gate，不是可選的「稍後再做」。同樣地，scaffold 的 in-memory resource 只適合 demo；在 readiness 之前先建立真實的 storage、cache、MQ lifecycle。

## TLS 在前面終結

服務的 port 是 cleartext h2c，伺服器本身不做 TLS。bearer token 經過這個 port 是明文的。把服務放在反向代理、service mesh 或 load balancer 後面終結 TLS，不要把 h2c port 直接暴露到公網。

## Readiness 接真實依賴

`/readyz` 預設只反映行程本身活著。用 `app.Health().Register(...)` 註冊 DB、cache、MQ 的真實檢查，readiness 才有參考價值：

```go
app.Health().Register("postgres", func(ctx context.Context) health.State {
    if err := pool.Ping(ctx); err != nil {
        return health.Unhealthy
    }
    return health.Healthy
})
```

每個 check 有 5 秒逾時，單一慢依賴不會拖垮整個查詢。

health probe 可綁在私有 ops 埠、與對外埠分離：`kernel.WithAdminListener(addr)` 在獨立的 plain-HTTP 埠只服務 `/healthz` 與 `/readyz`，讓 orchestrator 探測健康時不必碰到公開的 h2c 埠（`app.AdminAddr()` 可查實綁位址）。要在獨立埠上掛 pprof 或自訂 ops handler，用 `kernel.WithExtraListener(lis, h)`。兩者皆 opt-in，主 h2c 埠的單埠多工不變。

## 預設會記哪些 log

metric 與 span 在沒有設定 OTLP endpoint 時都會被直接丟掉，所以下面這幾件事各自會寫一行 WARN 到你的 logger，不需要任何遙測後端：

| 事件 | 訊息 | 附帶欄位 |
| --- | --- | --- |
| `/readyz` 回 503 | `gortexa: readiness not serving` | `status`、`checks` |
| 開機時 health registry 是空的 | `gortexa: no health checks registered; /readyz reports ready without probing anything` | — |
| 斷路器開／關 | `gortexa: circuit breaker state change` | `method`、`from`、`to` |
| MCP 拒絕 origin 或超量 batch | `mcp: rejected request from a disallowed origin` / `mcp: rejected an oversized JSON-RPC batch` | `origin`／`elements`＋`cap`、`remote` |

其中最值得盯的是空 registry 那行：它代表 `/readyz` 正在回報「就緒」但實際上什麼都沒探測。斷路器那行只有在用 `interceptor.NewSet` 時才會自動出現（它會用 `Config.Logger` 補上 `CBConfig.Logger`）；直接自建 breaker 的話要自己設。MCP 那兩行例外，不寫到「你的 logger」：bridge 寫到 `slog.Default()`，要讓它們走你設定的格式與 OTLP 管線，就呼叫 `slog.SetDefault(log)`。

## Authorization 是應用程式的事

框架只做 authentication：驗 JWT 簽章、把 claims 放進 context。誰能做什麼（role、permission policy）由應用程式在 handler 或自己的攔截器裡判斷：

```go
claims, ok := auth.ClaimsFrom(ctx)
if !ok || !slices.Contains(claims.Roles, "admin") {
    // 拒絕
}
```

## Graceful shutdown

收到 SIGTERM 後，伺服器在 `server.shutdown_timeout`（預設 20s）內排空 in-flight 請求再退出。排空逾時則強制關閉剩餘連線並硬停 gRPC server；逾時以 warning **記錄而非回傳錯誤**，行程仍以 0 退出、telemetry flush hooks 照跑——rolling deploy 時開著的 MCP SSE 或 `Health.Watch` 串流不會被當成 crash。負載中中斷的行為驗證見[範例 05](https://gortexa.sheng.page/examples/resilience/)。

## 生產旗標

- `server.reflection` 保持 `false`：不對外暴露服務結構。
- `observ.otlp_insecure` 連遠端 collector 時必須 `false`。
- 真實 secret 走 runtime injection，且不得存在於 Git 或 image layer，見[設定](https://gortexa.sheng.page/configuration/)。
