新增雲端版部署設定(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>
18 KiB
visionA — Stage 部署手冊
版本:Phase 0.7(2026-05-01) 對應任務:
.autoflow/progress.mdPhase 0.7 任務 B-1 ~ B-7 對應決策:S1-S8(HTTPS 處理位置、container 多 process、image 來源、OIDC public client 等)
1. 部署架構(一張圖)
[ Browser ]
│
│ HTTPS :9527 (Let's Encrypt 證書,公司自動續簽)
▼
┌─────────────────────────────────────────────────────────┐
│ 公司 host nginx(host: 192.168.0.130,stage-9527 域名) │
│ - HTTPS termination │
│ - reverse_proxy → http://localhost:9527 │
└─────────────────────────────────────────────────────────┘
│
│ HTTP :9527 (內網)
▼
┌─────────────────────────────────────────────────────────┐
│ Docker container: visiona (host:9527 → container:80) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ nginx :80 │ │
│ │ /healthz → 200 OK │ │
│ │ /api/* → api-server :3721 │ │
│ │ /storage/* → api-server :3721 │ │
│ │ /tunnel/connect→ remote-proxy :3800 (WS) │ │
│ │ /_next/static/ → next :3000 (1y cache) │ │
│ │ / → next :3000 (Next.js standalone) │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ ↓ ↓ │
│ next:3000 api-server:3721 remote-proxy:3800/3801 │
│ (node) (Go binary) (Go binary) │
└─────────────────────────────────────────────────────────┘
▲
│ wss://stage-9527.innovedus.com:9527/tunnel/connect
│
[ visionA Agent on user PC ]
OIDC 流程:
Browser ──→ /api/auth/login (visionA-backend)
──→ 302 to MC :7850/connect/authorize?... (PKCE)
──→ 使用者登入 MC
──→ 302 back to /api/auth/callback?code=... (visionA-backend)
──→ token exchange + 建 cookie session
──→ 302 to / (frontend)
2. 為什麼是這個架構(關鍵設計)
2.1 為什麼 HTTPS 由公司 host nginx 處理
公司 stage host 已有 host-level nginx + Let's Encrypt 自動續簽(stage-9527.innovedus.com 證書)。
我們重複實作 HTTPS 在 container 內反而:
- 無法續簽(container 內沒 LE client、無 ACME challenge 路徑)
- 與公司其他 service(
portainer、fanfan-mysql等共用 nginx)不一致
→ container 內 nginx 純 HTTP listen :80,由公司 host nginx 反代。
2.2 為什麼 container 多 process(不是 docker-compose 多 service)
仿 edge-ai-platform/docker/Dockerfile:
- POC 是這個模式,stage 階段「先讓它跑起來」優先於完美架構
- 公司 host nginx 只反代到 host:9527 一個 port → 容器內必須再有一層 nginx 分流
- 雛形階段不需要獨立擴容 frontend / api-server / remote-proxy
→ 一個 container 內 4 process(nginx + node + api-server + remote-proxy),由 entrypoint.stage.sh 起。
權衡:任一 process 死 → container 整個重啟(不像 docker-compose 個別 restart)。 但這也代表問題會被快速發現(healthcheck 立刻失敗),符合 stage 階段需求。
2.3 為什麼帶 node runtime(不是純 nginx serve static)
visionA-frontend/next.config.ts 設 output: "standalone"(不是 output: "export")。
差別:
| Config | 產出 | 部署 |
|---|---|---|
export |
out/ 全靜態 HTML |
nginx serve 即可(edge-ai-platform 用這個) |
standalone |
.next/standalone/server.js + .next/static/ |
需要 node 跑 server.js |
Frontend 動態 routes(/devices/[id]、/models/[id]、/workspace/[deviceId])目前是
「server component → unwrap params → 給 client component」pattern。改成 export 模式需要:
- 加
generateStaticParams()列出所有可能 ID(動態資源不可能列舉) - 或改用 placeholder
[_]路徑 + nginx fallback 重寫
→ stage 階段不動 frontend code(對齊產品線原則:「Phase 0 不發明新 UI」), container 多帶 node runtime 跑 standalone server。Image 大 ~70MB 可接受。
未來若改 S3 storage / 更嚴格的安全 audit,可考慮:
- frontend 走 CDN 靜態 export(需要先補
generateStaticParams或重整 routes) - 或 frontend 獨立 service(k8s deployment)
2.4 為什麼 image push internal registry(不是 SCP tar)
公司 192.168.0.130:5000 已有 registry:2 容器,與其他 service 共用 image registry。
Stage host 的 docker daemon 已配置 insecure-registries 信任。
→ 開發機 buildx push 到 internal registry → stage host docker compose pull 拉下來。 timestamp tag 留在 registry 給 rollback 用。
3. 環境前置(一次性)
3.1 開發機(執行 deploy-stage.sh 的機器)
# 必裝
docker --version # 任何 ≥ 20 版本
docker buildx version # 內建(Docker Desktop)或裝 docker-buildx-plugin
pnpm --version # ≥ 9(不是必需,scripts 在 container 內裝)
git --version
# 首次 push 到 internal registry 前要把它加進 daemon insecure-registries:
# macOS: Docker Desktop → Settings → Docker Engine
# 加進 daemon.json:
# {
# "insecure-registries": ["192.168.0.130:5000"]
# }
# 然後 Restart Docker Desktop
3.2 Stage host (192.168.0.130)
由公司 IT / DevOps 一次性設定(你不需要做,這裡列出供參考):
# 1. Docker daemon 已開 :2375 給內網 + insecure-registry 加 192.168.0.130:5000
# 2. 公司 host nginx 已配 server block:
# server {
# listen 9527 ssl;
# server_name stage-9527.innovedus.com;
# ssl_certificate /etc/letsencrypt/live/stage-9527.innovedus.com/fullchain.pem;
# ssl_certificate_key /etc/letsencrypt/live/stage-9527.innovedus.com/privkey.pem;
# location / {
# proxy_pass http://localhost:9527;
# proxy_http_version 1.1;
# proxy_set_header Host $host;
# proxy_set_header Upgrade $http_upgrade;
# proxy_set_header Connection "upgrade";
# proxy_read_timeout 86400s; # tunnel WS 用
# client_max_body_size 100M; # 模型上傳
# }
# }
# (注意:listen 9527 ssl 是公司 host nginx;
# proxy_pass http://localhost:9527 才是反代到我們 container 的 host:9527)
# 3. /opt/visiona/ 目錄存在且 docker daemon 可寫
# 4. /opt/visiona/data/ 存在(local-fs storage 用)
如果以上有缺,先請 IT 補上。第一次部署前請與 IT 確認 nginx 反代規則 + tunnel WS timeout 拉到 86400s(少了這個 visionA Agent 連線 1 小時就會被切)。
4. 第一次部署 step-by-step
4.1 開發機:產 .env.stage 並上傳到 stage host
cd /Users/jimchen/visionA
# 從範本複製
cp .env.stage.example .env.stage
# 產 secrets
SESSION_SECRET=$(openssl rand -hex 32)
SIGNING_SECRET=$(openssl rand -hex 32)
# 編輯,把以下兩個值換成上面產的
nano .env.stage
# VISIONA_SESSION_SECRET=<paste $SESSION_SECRET>
# VISIONA_STORAGE_SIGNING_SECRET=<paste $SIGNING_SECRET>
# .env.stage 已在 .gitignore,**不要 commit**
git status # 確認看不到 .env.stage
4.2 上傳 .env.stage + docker-compose.stage.yml 到 stage host
兩條路徑任選:
選項 A — SCP(如果有 SSH 權限)
# 假設 stage host 走 SSH(如有)
ssh stage-user@192.168.0.130 "mkdir -p /opt/visiona/data"
scp .env.stage docker-compose.stage.yml stage-user@192.168.0.130:/opt/visiona/
選項 B — Portainer / 手動
如果你只有 Docker daemon 存取(沒 SSH),請公司 IT 幫忙 mkdir + 把這兩個檔放到 /opt/visiona/:
# 把檔案內容複製到 stage host
cat .env.stage # 印出來給 IT 貼
cat docker-compose.stage.yml # 印出來給 IT 貼
4.3 確認 :9527 端口可用
# 從你的開發機,先確認 :9527 已釋放
DOCKER_HOST=tcp://192.168.0.130:2375 docker ps --filter publish=9527
# 應該空(edge-ai-platform 已 stop)
# 如果還有東西占著:
DOCKER_HOST=tcp://192.168.0.130:2375 docker stop <container-name>
4.4 跑部署
cd /Users/jimchen/visionA
bash scripts/deploy-stage.sh
預期輸出(每步驟有 === === 區隔):
=== 0/5 Pre-flight checks ===
[INFO] Project root : /Users/jimchen/visionA
[INFO] Docker remote : tcp://192.168.0.130:2375
[INFO] Registry : 192.168.0.130:5000
...
=== 1/5 Build (buildx, linux/amd64) ===
[+] Building 180.5s ...
[+] Pushing manifest for 192.168.0.130:5000/visiona:stage
=== 2/5 Verify image in registry ===
[INFO] registry 確認有 visiona:stage
=== 3/5 Remote pull image ===
=== 4/5 Deploy via docker compose ===
=== 5/5 Verify ===
[INFO] container healthy ✓
[INFO] 對外 healthz ✓ (https://stage-9527.innovedus.com:9527/healthz)
4.5 瀏覽器驗證
打開 https://stage-9527.innovedus.com:9527/:
- 看到 visionA 首頁(不是 nginx default 或 404)
- 點右上「登入」→ 跳到 MC OIDC 登入頁(
stage-9527.innovedus.com:7850) - 登入後 302 回 visionA
/,狀態列顯示已登入 - DevTools → Application → Cookies 看到
visiona_session(HttpOnly + Secure) https://stage-9527.innovedus.com:9527/healthz回ok
5. 日常更新部署
cd /Users/jimchen/visionA
git pull
bash scripts/deploy-stage.sh
腳本會:
- buildx build 新 image,push 到 registry(含 timestamp tag 給 rollback)
- stage host pull
:stagetag docker compose up -d重啟 container(restart unless-stopped)- 等 healthcheck
平均 wall-clock:約 3-5 分鐘(取決於有沒有改 dependency)。
6. Rollback
6.1 找前版 tag
# 列 registry 上的 stage tags
curl -s http://192.168.0.130:5000/v2/visiona/tags/list | python3 -m json.tool
# 或 docker remote 端
DOCKER_HOST=tcp://192.168.0.130:2375 docker images 192.168.0.130:5000/visiona --format "{{.Tag}}\t{{.CreatedSince}}"
Tag 格式:stage-YYYYMMDD-HHMMSS-<git_sha>,例:stage-20260501-153045-a1b2c3d
6.2 Rollback
bash scripts/deploy-stage.sh --rollback stage-20260501-153045-a1b2c3d
腳本會:
- 把
:stage-xxxretag 為:stage - push 回 registry
- stage host docker compose pull + up -d
如果 retag 推回失敗(registry 設 immutable),改成手動:
ssh stage-host
cd /opt/visiona
sed -i 's|visiona:stage|visiona:stage-20260501-153045-a1b2c3d|' docker-compose.stage.yml
docker compose -f docker-compose.stage.yml up -d
# 部署成功後改回來
sed -i 's|visiona:stage-.*|visiona:stage|' docker-compose.stage.yml
7. 故障排除
7.1 對外 https://stage-9527.innovedus.com:9527 不通
排除順序(由外而內):
# 0. 你網路本身能不能解到 stage-9527.innovedus.com?
nslookup stage-9527.innovedus.com
# 1. 公司 host nginx 有沒有監聽 :9527?
curl -k -v https://stage-9527.innovedus.com:9527/healthz
# 200 ok → 全鏈路 OK
# 502/504 → host nginx 有接到,但反代到 container 失敗(往下查 2)
# 連線拒絕 → host nginx 沒監聽 :9527(請 IT)
# 2. container 在不在?
DOCKER_HOST=tcp://192.168.0.130:2375 docker ps --filter name=visiona
# 看 STATUS:Up X minutes (healthy) 才正常
# 3. container 內部能不能打通?
DOCKER_HOST=tcp://192.168.0.130:2375 docker exec visiona curl -fsS http://127.0.0.1:80/healthz
# 應回 ok
# 4. host 能打到 container?
DOCKER_HOST=tcp://192.168.0.130:2375 docker exec visiona wget -qO- http://127.0.0.1:80/healthz
# 應回 ok
如果 step 1 是「連線拒絕」→ 公司 host nginx 沒接到 :9527,請 IT 加 server block(見 §3.2)。
7.2 OIDC callback 失敗
症狀:登入完跳回 /api/auth/callback 顯示 error。
檢查清單:
# 1. callback URL 三邊一致?
# .env.stage: VISIONA_OIDC_REDIRECT_URL
# MC client 設定: redirect_uris
# 瀏覽器網址列實際看到的: /api/auth/callback?code=...
# 三個必須完全一樣(含結尾斜線、含 :9527)
# 2. issuer URL 帶結尾斜線?
grep VISIONA_OIDC_ISSUER_URL /opt/visiona/.env.stage
# 必須是 https://stage-9527.innovedus.com:7850/ ← 有結尾斜線
# 3. client_secret 配對?
# Public PKCE-only client 必須 VISIONA_OIDC_CLIENT_SECRET 留空
# Confidential client 必須 VISIONA_OIDC_CLIENT_SECRET 有值
# 兩邊不對就會 invalid_client
# 4. 看 backend log
DOCKER_HOST=tcp://192.168.0.130:2375 docker logs visiona 2>&1 | grep -i oidc | tail -20
7.3 Cookie 沒被前端帶上
症狀:登入後 reload 又變未登入。
# DevTools → Network → /api/auth/me request → 看有沒有 Cookie: visiona_session=...
# 沒有 → cookie domain / path 設錯
# stage 應該 VISIONA_SESSION_COOKIE_DOMAIN=(留空,host-only)
# VISIONA_SESSION_COOKIE_SECURE=true(HTTPS only)
7.4 模型上傳卡在中段
症狀:100MB 內可以,超過會 413 或 timeout。
兩處 limit 必須對齊:
# 1. nginx.stage.conf(在 image 內)
grep client_max_body_size docker/nginx.stage.conf
# client_max_body_size 100M;
# 2. 公司 host nginx
# 請 IT 確認 server block 內也有 client_max_body_size 100M;
# 沒設 = 預設 1M,會 413
7.5 visionA Agent 連 tunnel 1 小時就斷
# 公司 host nginx 必須有:
# proxy_read_timeout 86400s;
# proxy_send_timeout 86400s;
# /tunnel/connect 同層 location 內也要有 Upgrade / Connection upgrade headers
# 請 IT 加上
7.6 Container 啟動就立刻 Exit
DOCKER_HOST=tcp://192.168.0.130:2375 docker logs visiona
# 常見原因:
# - Validate() fail:缺 OIDC env var → 看 stderr 列出哪個變數
# - VISIONA_SESSION_SECRET < 32 chars → backend panic
# - port 衝突:unable to bind :3721 → 看是不是別的 service 占了
7.7 緊急救回 POC(visionA 部 fail)
之前 stop 掉沒 rm,可救:
DOCKER_HOST=tcp://192.168.0.130:2375 docker stop visiona
DOCKER_HOST=tcp://192.168.0.130:2375 docker start edge-ai-platform
# :9527 流量回到 POC,先讓使用者繼續用
8. 已知限制(Phase 0.7)
- 公司 host nginx 是黑盒 — 不在我們 git 控管,更動需要請 IT,文件無法 100% 同步真實狀態
- 單 container 多 process — 4 個 process 共命運(任一掛 → 全重啟)。stage 階段可接受,production 應拆 service
- LocalFS storage — 上傳的模型存在 stage host
/opt/visiona/data/,container 重建不會丟(有 volume),但跨 host 移轉會丟 - OIDC 只接 login client — service-to-service
client_credentials已留 config 鉤子但未啟用 - Health check 只看 nginx,不看 backend —
/healthz是 nginx 直接回 200,不打 backend;backend 死了 nginx 還活著 healthy 仍是 healthy(但 entrypointwait -n會抓到 backend 死亡 → container 整個 die → docker restart) - Internal registry 沒設 push 認證 — 任何能 reach
192.168.0.130:5000的人都能 push 任何 image。stage 內網 OK,prod 必須加 auth - rollback retag —
deploy-stage.sh --rollback會把 timestamp tag retag 成:stage推回 registry。若 registry 啟用 immutable tag policy 會失敗,需手動改 compose file(§6.2 結尾有教)
9. 第一次部署 checklist(給使用者)
執行 deploy-stage.sh 之前,以下事項你需要做:
- 在開發機 Docker Desktop daemon.json 加
"insecure-registries": ["192.168.0.130:5000"] - 確認可以 reach
tcp://192.168.0.130:2375(docker -H tcp://192.168.0.130:2375 ps不拒連) - 確認可以 reach
192.168.0.130:5000(curl http://192.168.0.130:5000/v2/_catalog回 JSON) - 跟公司 IT 確認 stage host nginx 已配
:9527 → localhost:9527反代(含 WS upgrade + 86400s timeout + 100M body size) - 跟公司 IT 確認 stage host 上
/opt/visiona/目錄存在 - 在開發機產 .env.stage(
cp .env.stage.example .env.stage+ 編輯填 secrets) - 把
.env.stage+docker-compose.stage.yml上傳到 stage host/opt/visiona/ - 確認 :9527 已釋放(
edge-ai-platform已 stop) - MC 端 OIDC client
b8093fea1a504a5d8f0e04bee9f78f2e的 redirect_uri 是https://stage-9527.innovedus.com:9527/api/auth/callback
跑:
bash scripts/deploy-stage.sh
成功標準:
- 腳本 step 5/5 顯示
[INFO] container healthy ✓+[INFO] 對外 healthz ✓ - 瀏覽器打
https://stage-9527.innovedus.com:9527/看到 visionA 登入頁 - OIDC 流程跑得通
10. 相關檔案
| 檔案 | 角色 |
|---|---|
docker/Dockerfile.stage |
多 stage build:node + go + nginx 三 builder → nginx-alpine runtime |
docker/nginx.stage.conf |
container 內 nginx 反代規則 |
docker/entrypoint.stage.sh |
container 啟動腳本(4 process + signal handling) |
docker-compose.stage.yml |
stage host 上的 compose(單 service from registry) |
.env.stage.example |
環境變數範本(在 git 裡,可 commit) |
.env.stage |
真實 env 檔(不在 git 裡,由人手動放上 stage host) |
scripts/deploy-stage.sh |
一鍵部署腳本(buildx + push + remote compose) |
對照 dev 環境(不在本文件範圍):
| 檔案 | 角色 |
|---|---|
docker-compose.dev.yml |
local 開發(postgres + member-center + visiona-backend + frontend) |
.env.dev.example |
dev 環境範本 |
Makefile |
local make targets |
文件責任人:DevOps Agent 最後更新:2026-05-01 變更請同步更新
.autoflow/progress.mdPhase 0.7 任務狀態