visionA/docker/entrypoint.stage.sh
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

116 lines
4.1 KiB
Bash
Executable File
Raw 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.

#!/usr/bin/env bash
#
# visionA — stage container 內的多 process 啟動腳本
#
# 策略:
# 1. 啟動四個 process 為 background
# - api-server :3721
# - remote-proxy :3800/3801
# - node server.js :3000 (Next.js standalone)
# - nginx :80 (reverse proxy)
# 2. `wait -n` 阻塞,任一 process 結束 → 整個 container 退出(讓 Docker 重啟)
# 3. trap SIGTERM / SIGINT → 優雅關閉所有子 process
#
# 為什麼不用 supervisord
# - supervisord 預設會嘗試重啟死掉的子 process會掩蓋真正的錯誤healthcheck 看起來活著)
# - 在 stage 階段,「任一 process 死 → container die → docker restart unless-stopped」更乾淨
# - 由 Docker 層處理重啟,比 supervisord 內層重啟更容易看 log 與診斷
#
# 為什麼用 bashalpine 預設 sh 不支援 `wait -n`bash 4.3+ feature
# Dockerfile.stage 已 apk add bash。
set -euo pipefail
# ──────────── 環境變數預設值 ────────────
# 這些都可由 .env.stage 覆蓋;這裡只是「即使沒注入也能起來」的 fallback。
: "${VISIONA_API_PORT:=3721}"
: "${VISIONA_TUNNEL_PORT:=3800}"
: "${VISIONA_PROXY_INTERNAL_PORT:=3801}"
: "${VISIONA_PROXY_INTERNAL_URL:=http://127.0.0.1:${VISIONA_PROXY_INTERNAL_PORT}}"
: "${VISIONA_HOST:=0.0.0.0}"
# Next.js standalone server 預設讀 PORT / HOSTNAME
: "${NEXT_PORT:=3000}"
: "${NEXT_HOSTNAME:=127.0.0.1}"
# ──────────── helpers ────────────
log() {
# ISO8601 timestamp 方便 docker logs 排查
printf '[%s] [entrypoint] %s\n' "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" "$*"
}
# 子 process pid 暫存
PIDS=()
# 收到 SIGTERM/SIGINT → 廣播給子 process等它們收尾
shutdown() {
log "received signal — shutting down children: ${PIDS[*]:-}"
for pid in "${PIDS[@]:-}"; do
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
kill -TERM "$pid" 2>/dev/null || true
fi
done
# 給 30s 讓子 process 收尾,再強殺
local deadline=$(( $(date +%s) + 30 ))
while [ "$(date +%s)" -lt "$deadline" ]; do
local alive=0
for pid in "${PIDS[@]:-}"; do
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
alive=1
break
fi
done
[ "$alive" -eq 0 ] && break
sleep 1
done
for pid in "${PIDS[@]:-}"; do
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
log "force killing pid=$pid"
kill -KILL "$pid" 2>/dev/null || true
fi
done
exit 0
}
trap shutdown TERM INT
# ──────────── 起 process ────────────
log "starting remote-proxy on tunnel:${VISIONA_TUNNEL_PORT} internal:${VISIONA_PROXY_INTERNAL_PORT}"
/usr/local/bin/remote-proxy &
PIDS+=("$!")
# api-server 啟動會立即去探測 remote-proxy 的 internal HTTPPOC 假設)→ 給它一兩秒
sleep 1
log "starting api-server on :${VISIONA_API_PORT}"
/usr/local/bin/api-server &
PIDS+=("$!")
log "starting next.js standalone server on ${NEXT_HOSTNAME}:${NEXT_PORT}"
# Next.js 16 standalone 結構:
# /var/www/visiona/standalone/server.js
# /var/www/visiona/standalone/.next/static/ ← Dockerfile 已 COPY 進來
# /var/www/visiona/standalone/public/ ← Dockerfile 已 COPY 進來
# server.js 用環境變數 PORT / HOSTNAME 控制 listen address
(
cd /var/www/visiona/standalone
PORT="$NEXT_PORT" HOSTNAME="$NEXT_HOSTNAME" exec node server.js
) &
PIDS+=("$!")
log "starting nginx (foreground via daemon off)"
nginx -g 'daemon off;' &
PIDS+=("$!")
log "all process started: pids=${PIDS[*]}"
# ──────────── 等任一 process 退出 ────────────
# wait -n 在 bash 4.3+ 支援alpine 用 apk add bash 後可用。
# 任一 process 結束 → 整個 container 跟著結束 → docker restart 重起整套
wait -n
exit_code=$?
log "a child process exited (code=${exit_code}) — terminating container"
# 觸發 shutdown 把其餘 process 收尾
shutdown