visionA/docs/STAGE-DEPLOY.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

18 KiB
Raw Permalink Blame History

visionA — Stage 部署手冊

版本Phase 0.72026-05-01 對應任務:.autoflow/progress.md Phase 0.7 任務 B-1 ~ B-7 對應決策S1-S8HTTPS 處理位置、container 多 process、image 來源、OIDC public client 等)


1. 部署架構(一張圖)

[ Browser ]
    │
    │ HTTPS :9527  (Let's Encrypt 證書,公司自動續簽)
    ▼
┌─────────────────────────────────────────────────────────┐
│ 公司 host nginxhost: 192.168.0.130stage-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 路徑)
  • 與公司其他 serviceportainerfanfan-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 processnginx + node + api-server + remote-proxyentrypoint.stage.sh 起。

權衡:任一 process 死 → container 整個重啟(不像 docker-compose 個別 restart。 但這也代表問題會被快速發現healthcheck 立刻失敗),符合 stage 階段需求。

2.3 為什麼帶 node runtime不是純 nginx serve static

visionA-frontend/next.config.tsoutput: "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 獨立 servicek8s 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_sessionHttpOnly + Secure
  • https://stage-9527.innovedus.com:9527/healthzok

5. 日常更新部署

cd /Users/jimchen/visionA
git pull
bash scripts/deploy-stage.sh

腳本會:

  1. buildx build 新 imagepush 到 registry含 timestamp tag 給 rollback
  2. stage host pull :stage tag
  3. docker compose up -d 重啟 containerrestart unless-stopped
  4. 等 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

腳本會:

  1. :stage-xxx retag 為 :stage
  2. push 回 registry
  3. 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
# 看 STATUSUp 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

症狀:登入後 reload 又變未登入。

# DevTools → Network → /api/auth/me request → 看有沒有 Cookie: visiona_session=...
# 沒有 → cookie domain / path 設錯
#   stage 應該 VISIONA_SESSION_COOKIE_DOMAIN=留空host-only
#   VISIONA_SESSION_COOKIE_SECURE=trueHTTPS 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 緊急救回 POCvisionA 部 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

  1. 公司 host nginx 是黑盒 — 不在我們 git 控管,更動需要請 IT文件無法 100% 同步真實狀態
  2. 單 container 多 process — 4 個 process 共命運(任一掛 → 全重啟。stage 階段可接受production 應拆 service
  3. LocalFS storage — 上傳的模型存在 stage host /opt/visiona/data/container 重建不會丟(有 volume但跨 host 移轉會丟
  4. OIDC 只接 login client — service-to-service client_credentials 已留 config 鉤子但未啟用
  5. Health check 只看 nginx不看 backend/healthz 是 nginx 直接回 200不打 backendbackend 死了 nginx 還活著 healthy 仍是 healthy但 entrypoint wait -n 會抓到 backend 死亡 → container 整個 die → docker restart
  6. Internal registry 沒設 push 認證 — 任何能 reach 192.168.0.130:5000 的人都能 push 任何 image。stage 內網 OKprod 必須加 auth
  7. rollback retagdeploy-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:2375docker -H tcp://192.168.0.130:2375 ps 不拒連)
  • 確認可以 reach 192.168.0.130:5000curl 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.stagecp .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 buildnode + 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.md Phase 0.7 任務狀態