新增雲端版部署設定(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>
116 lines
4.1 KiB
Bash
Executable File
116 lines
4.1 KiB
Bash
Executable File
#!/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 與診斷
|
||
#
|
||
# 為什麼用 bash:alpine 預設 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 HTTP(POC 假設)→ 給它一兩秒
|
||
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
|