新增雲端版部署設定(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>
348 lines
12 KiB
Markdown
348 lines
12 KiB
Markdown
# 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 LTS(pnpm 需要)
|
||
```
|
||
|
||
---
|
||
|
||
## 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 API(OIDC IdP) |
|
||
| member-center-web | visiona-dev-member-center-web | **5060** | MC 後台管理 UI |
|
||
| member-center-init | visiona-dev-member-center-init | – | 一次性 job:DB 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_id(env 是 `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_id(GUID 格式)
|
||
- **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 dashboard,cookie 已設
|
||
|
||
---
|
||
|
||
## 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-local(local-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 執行階段問題
|
||
|
||
> 這節對應跑完 §5(OAuth 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:` header;frontend `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. 已知 limitation(Phase 0.6 雛形)
|
||
|
||
| Limitation | 影響 | 後續處理 |
|
||
|-----------|------|---------|
|
||
| MC admin API 無法注入 client_secret | 必須走 Web UI 建 OAuth client | MC 端開 issue:admin API 補 ClientSecret 欄位 |
|
||
| MC password grant 缺 sub claim → 500 | seed script 跑不通 | MC 端開 issue:TokenController 補 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 詳細用法
|