# visionA 部署指南 > 本檔聚焦「DB 接入版 backend 部署到 stage 並接真 PG/Redis」的端到端流程與煙測結果。 > stage 基礎設定(host nginx、HTTPS termination、docker daemon 連線)另見同目錄 `stage-deployment-setup.md`; > Phase 0.6 交接見 `phase-0.6-handover.md`。 ## 1. 架構總覽(DB 接入後) ``` 公司 host nginx (HTTPS termination, LE 證書) │ stage-9527.innovedus.com:9527 ▼ visiona container (image visiona:stage) ├─ 內層 nginx :80 ── 反代 ──► api-server :3721 (/api/*) │ ── 反代 ──► frontend standalone (Next.js) │ ── /healthz → return 200(淺層 liveness,不打 backend) │ ── /healthz/deep → 反代 api-server /healthz(深層 readiness,ping PG+Redis)見 §5/§9 ├─ api-server :3721 │ ├─ Postgres pool ──► 192.168.0.130:5432/visiona(真 PG) │ └─ Redis client ──► visiona-redis:6379/0(真 Redis) └─ remote-proxy :3800/:3801(tunnel) │ docker network: visiona-stage_default ├─ visiona 172.19.0.2 └─ visiona-redis (同網段,hostname 解析靠這個 network) ``` DB-on 啟用條件(`.env.stage` 已設): - Postgres:`VISIONA_DB_HOST` + `VISIONA_DB_USER` + `VISIONA_DB_NAME` 非空 → 自動建池 + auto-migrate + repository 切 Postgres。 - Redis:`VISIONA_REDIS_HOST` 非空(無密碼也算啟用)→ userSession 切 Redis、cookie session 持久化。 - `VISIONA_DB_AUTO_MIGRATE` 預設 **true**(`internal/config/load.go:109`)→ 啟動自動跑 `migrate up`。 ## 2. 前置需求 | 項目 | 內容 | |------|------| | docker daemon | stage host 130 開 `tcp://192.168.0.130:2375`,公司內網直連(VPN 大流量會卡,見 deploy-stage-v2.sh 註解) | | `.env.stage` | 含 DB + Redis + OIDC + storage 等 env,**git-ignored,不進 repo**(已驗證 `git check-ignore` 命中) | | 真 PG | 192.168.0.130:5432 db=`visiona` user=`vsausr` sslmode=`disable`,schema 已 migrate 到 version 3 | | 真 Redis | container `visiona-redis`,network `visiona-stage_default`,無密碼,6379 | | 部署腳本 | `scripts/deploy-stage-v2.sh`(remote build 模式,build 全在 stage daemon 跑) | ## 3. 部署步驟(DB 接入版) ### 3.1 network 接 visiona-redis — 怎麼解決的(關鍵) backend 用 hostname `visiona-redis` 連 Redis,**必須跟 visiona-redis 同 docker network**否則解析失敗。 解決方式:**靠 compose project name 自動對上**,不需改 compose。 - `deploy-stage-v2.sh` 用 `-p visiona-stage` 起 compose → 預設 network 名為 `visiona-stage_default`。 - `visiona-redis` 本來就在 `visiona-stage_default`(Up,label 顯示屬於同 stack)。 - 因此新 `visiona` container 起來自動落在同網段,`visiona-redis` hostname 直接可解。 驗證(部署前): ```bash export DOCKER_HOST=tcp://192.168.0.130:2375 docker run --rm --network visiona-stage_default redis:alpine redis-cli -h visiona-redis ping # → PONG docker run --rm --network visiona-stage_default postgres:16-alpine pg_isready -h 192.168.0.130 -p 5432 # → accepting connections ``` ⚠️ 注意:`docker-compose.stage.yml` 沒有顯式宣告 external network。目前能對上是因為「compose 自建的 default network 名稱 = visiona-redis 所在的 network 名稱」這個巧合(同 project name)。若未來 visiona-redis 改由別的 compose stack 管、或改 project name,需在 compose 顯式 `networks:` 接 external `visiona-stage_default`。建議後續硬化(見 §10)。 ### 3.2 部署指令 ```bash # 1. 先備份當前 image 供 rollback export DOCKER_HOST=tcp://192.168.0.130:2375 CURID=$(docker inspect visiona --format '{{.Image}}' | cut -c8-19) docker tag "$CURID" visiona:stage-rollback-pre-db # 2. build + deploy main HEAD(remote build,全在 stage daemon 跑) DOCKER_HOST=tcp://192.168.0.130:2375 bash scripts/deploy-stage-v2.sh ``` 部署會 `Recreate` visiona container(覆蓋舊版)。本次部署版本:**main HEAD 4d0b870**(DB 接入塊 0-5),image tag `visiona:stage` + `visiona:stage-20260620-184814-4d0b870`。 ## 4. 啟動煙測(本次實測,2026-06-20) 啟動 log 關鍵行(`.autoflow/07-delivery/logs/block6-container-startup-202606201848.log`): ``` postgres pool initialized target=192.168.0.130:5432/visiona sslmode=disable max_conns=10 min_conns=2 migrate up: applied version=3 migrations applied target=192.168.0.130:5432/visiona redis client initialized target=visiona-redis:6379/0 db=0 pairing/session token stores initialized backend=postgres user session store initialized backend=redis device repository initialized backend=postgres device unpairer initialized backend=postgres-tx model repository initialized backend=postgres conversion service initialized converter_base_url=http://192.168.0.130:9501 file access (FAA download) initialized (Phase 0.9 功能正常) api-server listening addr=0.0.0.0:3721 ``` 無 fatal / panic。所有 6 個 store 都顯示 `backend=postgres` / `backend=redis`(DB-on 模式生效)。 ## 5. healthz 行為(重要) > **2026-06-20 更新**:§8 的 nginx /healthz 不反映 DB 健康問題**已修復並重部署**(見 §8、§9)。 > 現在 stage 有兩條對外健康路由:淺層 `/healthz`(存活)與深層 `/healthz/deep`(readiness,反映 PG+Redis)。 | 路徑 | 回應(健康時) | 說明 | |------|------|------| | `https://stage-9527.innovedus.com:9527/healthz`(公開,淺層) | `200 "ok"` | nginx 直接 return,不打 backend、**不反映 DB**。用途:liveness / 「nginx 活著」探測。 | | `https://stage-9527.innovedus.com:9527/healthz/deep`(公開,深層)✅ 新增 | `200 {"checks":{"postgres":"ok","redis":"ok"},"status":"ok"}` | nginx `proxy_pass` 到 backend `/healthz`,**會 ping PG + Redis、down 回 503**。用途:**LB readiness**。 | | backend `:3721/healthz`(容器內) | `200 {"checks":{"postgres":"ok","redis":"ok"},"status":"ok"}` | 深層路由的後端來源,真正 ping PG + Redis。 | backend healthz 實作(`internal/api/health.go`):每次呼叫 ping PG + Redis,任一失敗回 **503**。 ### 5.1 503 fail-fast 實測(停 visiona-redis) log:`.autoflow/07-delivery/logs/block6-healthz-503-202606201848.log` ``` docker stop visiona-redis → backend :3721/healthz HTTP 503 {"checks":{"postgres":"ok","redis":"down"},"status":"unavailable"} docker start visiona-redis → backend :3721/healthz HTTP 200 {"checks":{"postgres":"ok","redis":"ok"},"status":"ok"} ``` - Redis down → 正確偵測 `redis:down` 回 503,PG 仍 `ok`。 - visiona container **沒有因 Redis 中斷而 crash**(fail-fast 只在啟動,runtime 降級僅反映在 healthz)。 - Redis 恢復後 healthz 自動回 200。 ## 6. 重啟資料持久化實測 log:`.autoflow/07-delivery/logs/block6-persistence-202606201848.log` ``` 插入 model row (name=block6-persist-...) 進 stage PG → rows_before_restart = 1 docker restart visiona → 啟動 log: "migrate up: no change (already at latest version)"(migration 冪等) → backend healthz 200 查詢 model row → rows_after_restart = 1(同 marker,資料還在) (測試資料已 cleanup) ``` 證明真部署環境下 DB-on 的資料跨 container 重啟持久化。 ## 7. 既有功能回歸 | 檢查 | 結果 | |------|------| | `GET /api/models`(公開,auth-gated) | `401`(api-server 活、DB-backed handler 可達,非 500/502) | | `GET /`(frontend) | `200` | | conversion service | 啟動 log 顯示 initialized(converter base url 正確) | | FAA download(Phase 0.9) | 啟動 log 顯示 initialized | ## 8. ✅ 已修復:nginx /healthz 不反映 DB 健康(+ docker healthcheck false 444) **原問題**:backend 的 DB-aware healthz(會 ping PG/Redis、down 回 503 的 fail-fast 邏輯)**實作了但 nginx 攔截不轉發**。 - `docker/nginx.stage.conf` 兩個 `location = /healthz` 都 `return 200 "ok"`,不 `proxy_pass` 到 api-server。 - 結果:對外 `/healthz` 永遠 200,即使 PG/Redis 掛了。backend 的 503 邏輯形同 dead code。 **併發現的第二個 bug(本次一併修)**:default_server block 結尾用 **server-level `return 444;`**,在 nginx rewrite phase 會「先於」location 匹配短路掉所有請求 —— 包含本該回 200 的 `location = /healthz`。導致 docker healthcheck(從 container 內打 `localhost/healthz`、來源 127.0.0.1)也被打成 **444**,container 長期顯示 `(unhealthy)`(FailingStreak 已累積到 96)。原 deploy-stage-v2.sh:196 把這當成「Host 白名單造成的 false negative」,實際成因是 `return 444` 的 phase 順序,不是 Host 白名單。 **修復內容(2026-06-20,commit 於 `docker/nginx.stage.conf`)**: 1. default_server 的 catch-all `return 444` 從 server-level 收進 `location / { return 444; }` —— 讓 exact-match `location = /healthz` 依 nginx location 優先序勝出。docker healthcheck 現回 200、container `healthy`。 2. 公開 server block 新增 `location = /healthz/deep` → `proxy_pass http://visiona_api/healthz`(深層 readiness,反映 PG+Redis)。 3. 保留淺層 `/healthz`(return 200)給 docker healthcheck / liveness。 **部署方式**:因 nginx config 在 image build 時 `COPY` 進去(非 bind mount),改 source 後以 `deploy-stage-v2.sh` **rebuild + recreate** 持久化(`nginx -s reload` 只是 runtime 暫補、container 重建即失效)。重部署後驗證 baked config 與 source 一致。 ## 9. healthz 接 load balancer(最終建議 — 已實作) 採 **選項 A**(liveness 淺 / readiness 深 分離,對齊 K8s 慣例)。已實作於 nginx: ```nginx # 公開 server block(server_name stage-9527.innovedus.com) location = /healthz/deep { access_log off; proxy_pass http://visiona_api/healthz; # api-server :3721,ping PG+Redis、down 回 503 proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header Connection ""; proxy_connect_timeout 2s; proxy_read_timeout 2s; proxy_send_timeout 2s; # 探針不該 hang proxy_no_cache 1; proxy_cache_bypass 1; add_header Cache-Control "no-store" always; } ``` **LB 設定建議**: - **health check path:`/healthz/deep`** ← LB readiness 打這條 - 期望狀態碼:200;**503 → 拉出輪替**(DB/Redis 掛時自動踢除實例) - interval 10–15s、timeout 5s、unhealthy threshold 連續 3 次(對齊 backend health.go 的 2s ping 逾時 + nginx 2s proxy 逾時) - 淺層 `/healthz` 保留給 docker healthcheck / liveness(DB 抖動時不會誤把實例標死) **實測(2026-06-20,log:`.autoflow/07-delivery/logs/healthz-deep-verify-*.log`)**: ``` PG+Redis 健康: 公開 /healthz → 200 "ok" 公開 /healthz/deep → 200 {"checks":{"postgres":"ok","redis":"ok"},"status":"ok"} docker healthcheck → healthy(streak=0;修復前 unhealthy streak=96) 停 visiona-redis(503 路徑): 公開 /healthz/deep → 503 {"checks":{"postgres":"ok","redis":"down"},"status":"unavailable"} ← LB 會踢 公開 /healthz → 200(淺層不受影響,liveness 不誤殺) 公開 /(frontend) → 200 起 visiona-redis(恢復): 公開 /healthz/deep → 200(自動恢復) ``` ## 10. 後續硬化建議(非本次範圍) 1. ~~**§8 healthz 接 LB**:依 §9 選項 A 加 `/healthz/deep`。~~ ✅ **已完成(2026-06-20)**,見 §8/§9。 2. **§3.1 network 顯式化**:在 `docker-compose.stage.yml` 顯式宣告 external network `visiona-stage_default`,去除「靠 project name 巧合對上」的隱性依賴。 3. **migration 與多副本**:目前單副本啟動跑 auto-migrate 沒問題;未來多副本需改 `VISIONA_DB_AUTO_MIGRATE=false` + 獨立 `cmd/migrate` 步驟,避免多實例同時 migrate。 ## 11. Rollback ```bash export DOCKER_HOST=tcp://192.168.0.130:2375 # 回到 DB 接入前的 Phase 0.9 image docker tag visiona:stage-rollback-pre-db visiona:stage bash scripts/deploy-stage-v2.sh --skip-build ``` (注意:rollback 到 DB 接入前版本後,repository 回 in-memory;PG/Redis 資料保留但不被讀。) ## 12. 煙測 evidence log 清單 | log | 內容 | |-----|------| | `.autoflow/07-delivery/logs/block6-deploy-202606201848.log` | build + compose up(exit 0) | | `.autoflow/07-delivery/logs/block6-container-startup-202606201848.log` | 啟動 log(DB/Redis/migration initialized) | | `.autoflow/07-delivery/logs/block6-healthz-503-202606201848.log` | 503 fail-fast 實測(停/起 redis) | | `.autoflow/07-delivery/logs/block6-persistence-202606201848.log` | 重啟資料持久化 + migration 冪等 |