visionA/docs/DEV-SETUP.md
jim800121chen eb66a7287a feat(deploy): visionA Cloud dev / stage docker compose + Caddy/nginx + 部署腳本
新增雲端版部署設定(Phase 0.6 dev + Phase 0.7 stage 分兩套):

dev 環境(docker-compose.dev.yml):
- 5 service all-in-one(postgres + member-center + visionA-backend + frontend + Caddy)
- Caddy 自動 HTTPS for localhost
- .env.dev.example 範本(使用者拷出 .env.dev 後 docker compose up -d)
- Makefile dev-with-mc 9 個 target

stage 環境(docker-compose.stage.yml + docker/Dockerfile.stage):
- multi-stage build(node22 frontend + go1.26 backend × 2 + nginx-alpine runtime)
  最終 image 319 MB,含 nginx + nodejs + tini + bash
- entrypoint.stage.sh 4 process 共命運(nginx + api-server + remote-proxy +
  next.js standalone)用 wait -n + SIGTERM trap
- nginx.stage.conf:白名單 server_name stage-9527.innovedus.com + 444 default_server
  + /healthz 例外(127.0.0.0/8 only)+ /api/ 與 /storage/ 強制 no-store
  + /tunnel/connect WS upgrade + 100M body / 3600s timeout
- 對外 mapping 0.0.0.0:9527:80(公司 host nginx 在外層處理 HTTPS termination
  — Let's Encrypt stage-9527.innovedus.com 自動續簽)
- named volume visiona-data(不用 bind mount,因 stage docker daemon 在 host root
  無 mkdir 權限)

部署腳本(scripts/deploy-stage.sh):
- 仿 edge-ai-platform/scripts/deploy-docker.sh 早期 save/load 模式
- 為什麼不用 internal registry:公司 192.168.0.130:5000 開了 auth、無帳密
- 流程:buildx --load → docker save | gzip → DOCKER_HOST docker load → compose up
- 含 --rollback <tag> / --skip-build / --no-push / --skip-deploy 選項
- timestamp + git SHA tag 留 rollback 餘地

文件(docs/):
- DEV-SETUP.md:dev 環境一鍵起步驟
- SMOKE-TEST.md:手動煙測 checklist(OIDC flow / pairing / tunnel)
- STAGE-DEPLOY.md:stage 完整手冊(架構圖 / 環境前置 / 部署 step / rollback /
  7 種故障排除 / 緊急救回 POC)

.env.stage.example 對齊 backend A1 改造:
- VISIONA_OIDC_CLIENT_SECRET 留空(PKCE-only public client)
- VISIONA_OIDC_SERVICE_CLIENT_ID/_SECRET 留空(Phase 1 預留鉤子)
- 所有 secret 用 placeholder(CHANGE_ME_OPENSSL_RAND_HEX_32)

.dockerignore:避免 node_modules / .next / .git 等進 build context

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 11:22:44 +08:00

348 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# visionA Dev 環境設置(一鍵 docker compose
> 對應 `.autoflow/04-architecture/oidc-tdd.md` §12 + Phase 0.6 OD1 任務
>
> 適用對象:剛 clone 下來、第一次想跑起 visionA + Member Center OIDC flow 的開發者
>
> 預期時間:第一次 build ~10 分鐘;之後重啟 < 30 秒
---
## TL;DR
```bash
# 1. 確認 ../member_center 與本 repo 平行擺放
# visionA/ 與 member_center/ 在同一個 parent dir
ls -d ../member_center # 應該存在
# 2. 複製 env 範本
cp .env.dev.example .env.dev
# 視情況編輯(多半預設值即可)
# 3. 起所有 service
docker compose -f docker-compose.dev.yml --env-file .env.dev up -d --build
# 第一次:~10 分鐘dotnet 8 build + go build
# 之後 < 30 秒
# 4. 等所有 service healthy< 1 分鐘)
docker compose -f docker-compose.dev.yml ps
# 全部要看到 (healthy)
# 5. 註冊 visionA OAuth client手動~2 分鐘,見下方第 5 節)
# 6. 把 client_id 寫進 .env.dev → 重啟 api
docker compose -f docker-compose.dev.yml --env-file .env.dev up -d visiona-api
# 7. 另開 terminal 跑 frontend
cd visionA-frontend && pnpm install && pnpm dev
# 8. 開 http://localhost:3000 → 點登入 → 完成 OIDC flow
```
---
## 1. 前置需求
| 工具 | 版本 | 安裝 |
|------|------|------|
| Docker Desktop | 最新 | https://docker.com/products/docker-desktop |
| Docker Compose | v2.30+ | 通常隨 Docker Desktop |
| pnpm | 9+ | `brew install pnpm``npm i -g pnpm` |
| jq | 任意 | `brew install jq`auto seed script 用)|
確認:
```bash
docker --version # >= 24.x
docker compose version # >= v2.30
node --version # >= 18 LTSpnpm 需要)
```
---
## 2. 目錄結構假設
```
parent/
├── visionA/ ← 你在這this repo
│ ├── docker-compose.dev.yml
│ ├── .env.dev.example
│ ├── visionA-backend/
│ └── visionA-frontend/
└── member_center/ ← 平行擺放(用 git clone
└── src/MemberCenter.Api/Dockerfile
```
如果你的 `member_center` 在別處:
```bash
# 編輯 .env.dev
MEMBER_CENTER_PATH=/Users/me/code/member_center
```
---
## 3. 服務拓撲
| Service | Container | Host port | 用途 |
|---------|-----------|-----------|------|
| postgres | visiona-dev-postgres | 5432 | MC 的資料庫 |
| member-center | visiona-dev-member-center | **5050** | MC APIOIDC IdP |
| member-center-web | visiona-dev-member-center-web | **5060** | MC 後台管理 UI |
| member-center-init | visiona-dev-member-center-init | | 一次性 jobDB migrate + 建 admin |
| visiona-proxy | visiona-dev-proxy | 3800 (tunnel WS) | local agent 連入 |
| visiona-api | visiona-dev-api | **3721** | visionA 後端 API |
| frontend | host pnpm dev | **3000** | Next.js dev server |
---
## 4. 起環境步驟
### 4.1 第一次(含 build
```bash
cd /path/to/visionA
cp .env.dev.example .env.dev
# Build 所有 image耐心等 ~10 分鐘)
docker compose -f docker-compose.dev.yml --env-file .env.dev build
# 起來
docker compose -f docker-compose.dev.yml --env-file .env.dev up -d
```
### 4.2 確認都健康
```bash
docker compose -f docker-compose.dev.yml ps
```
預期看到:
```
NAME STATUS
visiona-dev-postgres Up (healthy)
visiona-dev-member-center Up (healthy)
visiona-dev-member-center-web Up
visiona-dev-member-center-init Exited (0) ← 正常,一次性 job
visiona-dev-proxy Up (healthy)
visiona-dev-api Up (healthy)
```
如果 visiona-api 卡在 unhealthy / restarting → 看 §7 故障排除。
### 4.3 確認 OIDC discovery 通
```bash
curl -s http://localhost:5050/.well-known/openid-configuration | head -c 200
```
應該看到 JSON`"issuer":"http://localhost:5050/"`
---
## 5. 註冊 visionA OAuth Client**必做**
第一次 build 起來時 visionA-api 還沒有合法的 OIDC client_idenv 是 `CHANGE_ME`),點登入會 fail。
需先在 MC 註冊一個 OAuth client。
### 5.1 走 MC Web UI推薦目前唯一可行路徑
> 為什麼不用 script 自動化MC password grant 在 Identity user 上有個 bug`mandatory subject claim was missing`),無法用 admin 帳密拿 token 呼叫 admin API。已於 MC 端開 issue。
**步驟**
1. **開 MC 後台**http://localhost:5060
2. **登入**
- Email: `admin@visiona.local`
- Password: `Admin12345!`
- (這組密碼來自 `.env.dev``MC_ADMIN_EMAIL` / `MC_ADMIN_PASSWORD`
3. **建 Tenant**
- 進「Tenants」頁
- 點「Create」/「新增」
- Name: `visionA`
- Domains: `visiona.cloud, localhost`
- Status: `active`
- 送出
4. **建 OAuth Client**
- 進「OAuth Clients」頁
- 點「Create」
- Tenant: 選剛剛建的 `visionA`
- Name: `visionA Cloud`
- Client Type: `confidential`
- Usage: `webhook_outbound`
- MC 目前沒有 `web_app` 類型,雛形借用 `webhook_outbound`,是唯一允許 redirect_uris 的非 client-credentials usage對應 oidc-tdd §11.2
- Redirect URIs: `http://localhost:3721/api/auth/callback`
- 送出
5. **取 client_id / client_secret**
- 建立成功後,畫面會顯示 client_idGUID 格式)
- **client_secret**MC 應該會顯示一次(如果 UI 沒給 secret是 MC 的另一個 limitation雛形階段可改成 `client_type=public` 不帶 secret
### 5.2 把 client_id / secret 寫進 .env.dev
```bash
# 編輯 .env.dev
VISIONA_OIDC_CLIENT_ID=<剛剛拿到的 client_id GUID>
VISIONA_OIDC_CLIENT_SECRET=<剛剛拿到的 secret沒拿到就留空>
```
### 5.3 重啟 visiona-api
```bash
docker compose -f docker-compose.dev.yml --env-file .env.dev up -d visiona-api
docker compose -f docker-compose.dev.yml logs visiona-api --tail 5
```
預期看到:
```
{"msg":"OIDC initialized","issuer":"http://localhost:5050/","client_id":"<你的 guid>","redirect_url":"http://localhost:3721/api/auth/callback"}
{"msg":"api-server listening","addr":"0.0.0.0:3721"}
```
---
## 6. 跑 Frontend + 完成 demo flow
```bash
# 另開 terminal
cd visionA-frontend
pnpm install
pnpm dev
# → http://localhost:3000
```
**Demo flow**
1. 開 http://localhost:3000
2. 點「登入」按鈕(會跳到 backend /api/auth/login
3. backend 302 到 MC `/oauth/authorize?...`
4. MC 顯示登入頁 → 用 `admin@visiona.local / Admin12345!` 登入
(或先在 MC Web UI 建 demo user 帳號)
5. 同意授權後 redirect 回 `http://localhost:3721/api/auth/callback?code=...`
6. backend 換 token、驗 id_token、建 session
7. 302 回 frontend dashboardcookie 已設
---
## 7. 故障排除
### 7.1 visiona-api 卡 unhealthy / restarting
最常見OIDC 沒設好 → backend crash loop。
```bash
docker compose -f docker-compose.dev.yml logs visiona-api --tail 30
```
| 錯誤關鍵字 | 原因 | 解法 |
|----------|------|------|
| `discovery fetch failed` | MC 還沒起來 | 等 30 秒再看;或 `docker compose ps member-center` 確認 healthy |
| `issuer URL ... did not match` | issuer trailing slash 不一致 | 確認 `.env.dev``VISIONA_OIDC_ISSUER_URL=http://localhost:5050/`**斜線結尾** |
| `IssuerURL invalid` / `ClientID required` | env 沒帶到 | 確認啟動時用 `--env-file .env.dev` |
| `connection refused` | container 內找不到 host | 不應該發生compose 已加 `extra_hosts: localhost:host-gateway` |
### 7.2 port 衝突
| port | 衝突來源 | 解法 |
|------|---------|-----|
| 3721 | 本機跑著 visionA-locallocal-tool 那一邊) | 暫停 local-tool或在 `.env.dev``VISIONA_API_PORT=3722` 改一下、frontend 端 base URL 也改 |
| 5432 | 本機 PostgreSQL | `.env.dev``POSTGRES_PORT=5433` |
| 5050 | 已被占用 | `.env.dev``MEMBER_CENTER_PORT=5051` + `VISIONA_OIDC_ISSUER_URL=http://localhost:5051/` |
| 5060 | 已被占用 | `.env.dev``MEMBER_CENTER_WEB_PORT=5061` |
### 7.3 MC build 失敗 / Dockerfile 錯誤
如果 `MEMBER_CENTER_PATH` 路徑不對:
```bash
# 確認 path
ls -d ${MEMBER_CENTER_PATH:-../member_center}
# 用絕對路徑
MEMBER_CENTER_PATH=/Users/you/code/member_center docker compose -f docker-compose.dev.yml build member-center
```
### 7.4 MC password grant 500 error
```
System.InvalidOperationException: The specified principal was rejected
because the mandatory subject claim was missing.
```
**已知 MC bug**Identity user 缺 sub claim。Phase 0.6 雛形請走 admin Web UI 註冊 OAuth client不用 password flow。
### 7.5 一鍵完全重置
```bash
docker compose -f docker-compose.dev.yml down -v
# -v 會刪除 volumes包括 postgres data + visiona storage
# 下次 up -d 時會重新跑 member-center-init 建 admin 帳號
```
### 7.6 OIDC flow 執行階段問題
> 這節對應跑完 §5OAuth client 註冊)後,實際走 login flow 卻卡住的情境。
> 詳細逐步驗證見 `docs/SMOKE-TEST.md`,這裡只列高頻錯誤對照表。
| 症狀 | 通常原因 | 解法 |
|------|---------|------|
| 點登入後 callback 噴 `state_mismatch` | `visiona_pending_sid` cookie 沒帶過去 | 確認 `VISIONA_FRONTEND_URL` 與 frontend 真實 URL 一致;不要設 cookie `Domain`;瀏覽器 DevTools → Application → Cookies 看 cookie 是否存進去 |
| callback 噴 `pending_session_not_found` | pending 過 10 分鐘 TTL或 backend 重啟清掉 in-memory store | 重新點登入;雛形 in-memory 重啟即清空 |
| callback 噴 `id_token_invalid` | iss / aud / nonce 對不上,或 JWKS cache 過時 | 看 backend log `detail` 欄;嘗試 `docker compose restart visiona-api` 清 JWKS cache |
| callback 噴 `token_exchange_failed` | client_secret 不對 / redirect_uri 與 MC 設定不一致 | 比對 `.env.dev` 與 MC admin UI 中 client 設定 |
| `GET /api/auth/me` 永遠 401 | cookie 沒帶 / cookie 簽章對不上 / session 過期 | DevTools 看 request 是否帶 `Cookie:` headerfrontend `fetch` 必須 `credentials: 'include'``VISIONA_SESSION_SECRET` 改過會讓舊 cookie 失效 → 清 cookie 重登 |
| Pairing token 綁的 user_id 是 `demo-user` 而非 OIDC sub | `VISIONA_AUTH_TYPE` 還是 `static`,或 OB5 沒拔乾淨 | 確認 `.env.dev``VISIONA_AUTH_TYPE=oidc` 且重啟過 visiona-api`docker compose logs visiona-api \| grep -i pairing` 看實際 binding |
| 登出後仍能訪問受保護頁面 | `Set-Cookie: visiona_session=; Max-Age=0` 沒設對,或 frontend route guard 沒擋 | DevTools 確認 cookie 真的被清;訪 `/api/auth/me` 不帶 cookie 應 401 |
---
## 8. 常用命令
```bash
# 看所有 service log
docker compose -f docker-compose.dev.yml logs -f
# 看單一 service
docker compose -f docker-compose.dev.yml logs -f visiona-api
# 重啟單一 service
docker compose -f docker-compose.dev.yml restart visiona-api
# 進 postgres
docker exec -it visiona-dev-postgres psql -U postgres -d membercenter
# 看 OpenIddict apps
docker exec -it visiona-dev-postgres psql -U postgres -d membercenter \
-c 'SELECT "ClientId", "DisplayName", "ClientType", "RedirectUris" FROM "OpenIddictApplications";'
# 完全停止(保留 data
docker compose -f docker-compose.dev.yml down
# 完全停止 + 清乾淨
docker compose -f docker-compose.dev.yml down -v
# 重 build程式碼變了
docker compose -f docker-compose.dev.yml up -d --build visiona-api
```
---
## 9. 已知 limitationPhase 0.6 雛形)
| Limitation | 影響 | 後續處理 |
|-----------|------|---------|
| MC admin API 無法注入 client_secret | 必須走 Web UI 建 OAuth client | MC 端開 issueadmin API 補 ClientSecret 欄位 |
| MC password grant 缺 sub claim → 500 | seed script 跑不通 | MC 端開 issueTokenController 補 Subject claim mapping |
| MC 只有 `webhook_outbound` 支援 redirect_uris | usage 命名語意不對(雛形借用) | MC 端開 issue新增 `web_app` usage對應 oidc-tdd §11.2 |
| host visiona-local 占 3721 → 跟 docker compose 衝突 | 兩個只能擇一跑 | 預期docs 已說明 |
---
## 10. 相關文件
- `.autoflow/04-architecture/oidc-tdd.md`OIDC 接入 TDD§11 MC 端、§12 docker-compose、§13 env
- `.autoflow/04-architecture/adr/adr-010-oidc-bff.md`BFF 架構決策
- `visionA-backend/.env.example`visionA-backend 完整 env 列表
- `~/member_center/docs/INSTALL.md`MC installer 詳細用法