新增雲端版部署設定(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>
278 lines
11 KiB
Bash
Executable File
278 lines
11 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
#
|
||
# visionA — Stage Deploy(save/load 模式,仿 edge-ai-platform/scripts/deploy-docker.sh 早期版本)
|
||
#
|
||
# 為什麼不用 internal registry:
|
||
# 公司 192.168.0.130:5000 registry 開了 auth(401 UNAUTHORIZED),且 visionA dev 端沒帳密。
|
||
# 2026-05-01 改走 buildx --load → docker save | gzip → DOCKER_HOST docker load 模式,
|
||
# 完全不依賴 registry。Phase 1 拿到 registry 帳密後可改回 push 模式(grep 本檔的 SAVE_LOAD 區塊)。
|
||
#
|
||
# 流程:
|
||
# 0. 前置檢查:docker buildx、git status、必要工具
|
||
# 1. buildx build linux/amd64 --load 到本機 docker
|
||
# 2. docker save | gzip > /tmp/visiona-stage.tar.gz
|
||
# 3. DOCKER_HOST=tcp://192.168.0.130:2375 docker load < /tmp/visiona-stage.tar.gz
|
||
# 4. DOCKER_HOST=... docker compose up -d
|
||
# 5. healthcheck:curl https://stage-9527.innovedus.com:9527/healthz
|
||
#
|
||
# 用法:
|
||
# bash scripts/deploy-stage.sh # build + deploy
|
||
# bash scripts/deploy-stage.sh --skip-build # 不重 build,直接 redeploy 已 load 的 :stage
|
||
# bash scripts/deploy-stage.sh --rollback <tag> # rollback 到指定 timestamp tag(須仍存在 stage docker)
|
||
# bash scripts/deploy-stage.sh --no-push # 只 build 不 save/load(local 驗證用)
|
||
# bash scripts/deploy-stage.sh --help
|
||
|
||
set -euo pipefail
|
||
|
||
# ──────────── 路徑 ────────────
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||
DOCKERFILE="$PROJECT_ROOT/docker/Dockerfile.stage"
|
||
COMPOSE_FILE="$PROJECT_ROOT/docker-compose.stage.yml"
|
||
|
||
# ──────────── stage 設定(以使用者 2026-05-01 提供值為準)────────────
|
||
DOCKER_REMOTE="${DOCKER_HOST:-tcp://192.168.0.130:2375}"
|
||
IMAGE_NAME="visiona"
|
||
IMAGE_TAG="stage"
|
||
# 不再用 internal registry — image 透過 docker save/load 直接搬到 stage docker
|
||
# 沒有 REGISTRY prefix 時 docker 會把 image 留在本地 daemon,docker-compose 也以此為基準
|
||
IMAGE_REF="${IMAGE_NAME}:${IMAGE_TAG}"
|
||
SAVE_TARBALL="/tmp/${IMAGE_NAME}-${IMAGE_TAG}.tar.gz"
|
||
STAGE_HOST_PATH="/opt/visiona" # stage host 上 compose file + .env 放置目錄
|
||
STAGE_DOMAIN="stage-9527.innovedus.com"
|
||
STAGE_PORT="9527"
|
||
HEALTHZ_URL="https://${STAGE_DOMAIN}:${STAGE_PORT}/healthz"
|
||
|
||
# ──────────── colors ────────────
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
RED='\033[0;31m'
|
||
CYAN='\033[0;36m'
|
||
BLUE='\033[0;34m'
|
||
NC='\033[0m'
|
||
|
||
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; }
|
||
step() { echo -e "\n${CYAN}=== $* ===${NC}\n"; }
|
||
hint() { echo -e "${BLUE}[HINT]${NC} $*"; }
|
||
|
||
show_help() {
|
||
cat <<'HELP'
|
||
visionA — Stage Deploy
|
||
|
||
Usage:
|
||
bash scripts/deploy-stage.sh [OPTIONS]
|
||
|
||
Options:
|
||
--skip-build Skip buildx step, redeploy existing :stage tag
|
||
--no-push Build only, do not push to internal registry (local validation)
|
||
--rollback <tag> Rollback to specified timestamp tag (e.g. stage-20260501-153045)
|
||
--skip-deploy Build & push only, do not run remote compose
|
||
--help Show this help
|
||
|
||
Environment:
|
||
DOCKER_HOST Remote Docker daemon (default: tcp://192.168.0.130:2375)
|
||
|
||
Examples:
|
||
bash scripts/deploy-stage.sh
|
||
bash scripts/deploy-stage.sh --skip-build
|
||
bash scripts/deploy-stage.sh --rollback stage-20260501-153045
|
||
bash scripts/deploy-stage.sh --no-push # local Dockerfile 驗證
|
||
|
||
After deployment, browse:
|
||
https://stage-9527.innovedus.com:9527/
|
||
HELP
|
||
}
|
||
|
||
# ──────────── parse args ────────────
|
||
SKIP_BUILD=false
|
||
NO_PUSH=false
|
||
SKIP_DEPLOY=false
|
||
ROLLBACK_TAG=""
|
||
|
||
while [ $# -gt 0 ]; do
|
||
case "$1" in
|
||
--skip-build) SKIP_BUILD=true; shift ;;
|
||
--no-push) NO_PUSH=true; shift ;;
|
||
--skip-deploy) SKIP_DEPLOY=true; shift ;;
|
||
--rollback) ROLLBACK_TAG="$2"; shift 2 ;;
|
||
--help|-h) show_help; exit 0 ;;
|
||
*) error "Unknown option: $1 (use --help)" ;;
|
||
esac
|
||
done
|
||
|
||
# rollback mode:跳過 build,直接 deploy 指定 tag
|
||
if [ -n "$ROLLBACK_TAG" ]; then
|
||
SKIP_BUILD=true
|
||
NO_PUSH=true
|
||
info "Rollback mode: deploying $REGISTRY/$IMAGE_NAME:$ROLLBACK_TAG"
|
||
fi
|
||
|
||
# ──────────── pre-flight ────────────
|
||
step "0/5 Pre-flight checks"
|
||
|
||
command -v docker >/dev/null 2>&1 || error "docker 未安裝"
|
||
docker buildx version >/dev/null 2>&1 || error "docker buildx 未安裝(macOS: 內建;Linux: docker-buildx-plugin)"
|
||
|
||
if [ "$SKIP_BUILD" = false ]; then
|
||
[ -f "$DOCKERFILE" ] || error "找不到 Dockerfile.stage:$DOCKERFILE"
|
||
fi
|
||
[ -f "$COMPOSE_FILE" ] || error "找不到 docker-compose.stage.yml:$COMPOSE_FILE"
|
||
|
||
# git 檢查(warn only,stage 部署允許 dirty)
|
||
if [ "$SKIP_BUILD" = false ] && [ -z "${ROLLBACK_TAG}" ]; then
|
||
cd "$PROJECT_ROOT"
|
||
if ! git diff-index --quiet HEAD -- 2>/dev/null; then
|
||
warn "git working tree 有未 commit 的變更,stage build 仍會繼續"
|
||
warn "建議先 commit,否則 image 與 git SHA 對不上難以追蹤"
|
||
if [ -t 0 ]; then
|
||
read -r -p "繼續嗎? [y/N] " yn
|
||
[[ "$yn" =~ ^[Yy]$ ]] || error "取消(請先 commit 或加 --skip-build)"
|
||
else
|
||
warn "non-interactive shell — 自動繼續"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
GIT_SHA="$(cd "$PROJECT_ROOT" && git rev-parse --short HEAD 2>/dev/null || echo unknown)"
|
||
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
|
||
TIMESTAMPED_TAG="${IMAGE_TAG}-${TIMESTAMP}-${GIT_SHA}"
|
||
|
||
info "Project root : $PROJECT_ROOT"
|
||
info "Docker remote : $DOCKER_REMOTE"
|
||
info "Image : $IMAGE_REF"
|
||
[ -n "$ROLLBACK_TAG" ] && info "Rollback tag : ${IMAGE_NAME}:${ROLLBACK_TAG}"
|
||
[ -z "$ROLLBACK_TAG" ] && info "Timestamp tag : ${IMAGE_NAME}:${TIMESTAMPED_TAG}"
|
||
info "Git SHA : $GIT_SHA"
|
||
|
||
# ──────────── SAVE_LOAD 區塊:build → save → load ────────────
|
||
# 為什麼這樣做:公司 internal registry 開了 auth,dev 端無帳密 → 改走 docker save/load
|
||
# Phase 1 若拿到 registry 帳密,可改回 buildx --push + remote docker pull(grep "SAVE_LOAD")
|
||
if [ "$SKIP_BUILD" = false ]; then
|
||
step "1/5 Build (buildx, linux/amd64, --load 進本機 daemon)"
|
||
|
||
BUILDX_FLAGS=(
|
||
--platform linux/amd64
|
||
-f "$DOCKERFILE"
|
||
-t "${IMAGE_NAME}:${IMAGE_TAG}"
|
||
-t "${IMAGE_NAME}:${TIMESTAMPED_TAG}"
|
||
--load
|
||
)
|
||
|
||
cd "$PROJECT_ROOT"
|
||
docker buildx build "${BUILDX_FLAGS[@]}" .
|
||
info "build 完成:本機已有 ${IMAGE_NAME}:${IMAGE_TAG} 與 ${IMAGE_NAME}:${TIMESTAMPED_TAG}"
|
||
|
||
if [ "$NO_PUSH" = false ]; then
|
||
step "2/5 Save image → 傳到 stage docker(save | load 模式)"
|
||
|
||
info "docker save | gzip → ${SAVE_TARBALL}(含 :${IMAGE_TAG} + :${TIMESTAMPED_TAG} 兩個 tag)"
|
||
# 同時 save 兩個 tag — load 後 stage docker 會兩個都有,方便 rollback
|
||
docker save "${IMAGE_NAME}:${IMAGE_TAG}" "${IMAGE_NAME}:${TIMESTAMPED_TAG}" \
|
||
| gzip > "$SAVE_TARBALL"
|
||
SIZE=$(du -h "$SAVE_TARBALL" | awk '{print $1}')
|
||
info "tarball 產出:$SIZE @ $SAVE_TARBALL"
|
||
|
||
info "DOCKER_HOST=$DOCKER_REMOTE docker load(透過 TCP 傳輸到 stage daemon)..."
|
||
gunzip -c "$SAVE_TARBALL" | DOCKER_HOST="$DOCKER_REMOTE" docker load
|
||
info "stage docker daemon 已 load 完成"
|
||
|
||
# 清掉 tarball(內含完整 image 內容)— 不該留在 dev 機 /tmp
|
||
rm -f "$SAVE_TARBALL"
|
||
info "清除本機 tarball:$SAVE_TARBALL"
|
||
else
|
||
info "skip step 2/5(--no-push)"
|
||
fi
|
||
else
|
||
info "skip 1/5, 2/5(--skip-build)"
|
||
fi
|
||
|
||
# ──────────── deploy ────────────
|
||
if [ "$SKIP_DEPLOY" = true ] || [ "$NO_PUSH" = true ]; then
|
||
info "skip 3/5, 4/5, 5/5(--skip-deploy 或 --no-push)"
|
||
info "下一步:在 stage host 上手動執行"
|
||
hint " ssh stage-host"
|
||
hint " cd $STAGE_HOST_PATH"
|
||
hint " docker compose -f docker-compose.stage.yml up -d"
|
||
exit 0
|
||
fi
|
||
|
||
step "3/5 Tag rollback target(rollback mode only)"
|
||
|
||
# rollback mode:把 stage docker 上既存的 timestamp tag 改名成 :stage(save/load 模式不需要 push)
|
||
if [ -n "$ROLLBACK_TAG" ]; then
|
||
info "rollback:把 stage docker 上的 ${IMAGE_NAME}:${ROLLBACK_TAG} retag 為 ${IMAGE_NAME}:${IMAGE_TAG}"
|
||
|
||
if ! DOCKER_HOST="$DOCKER_REMOTE" docker image inspect "${IMAGE_NAME}:${ROLLBACK_TAG}" >/dev/null 2>&1; then
|
||
error "rollback tag 不存在於 stage docker:${IMAGE_NAME}:${ROLLBACK_TAG}
|
||
可用 tag list 查看:DOCKER_HOST=$DOCKER_REMOTE docker images $IMAGE_NAME"
|
||
fi
|
||
|
||
DOCKER_HOST="$DOCKER_REMOTE" docker tag \
|
||
"${IMAGE_NAME}:${ROLLBACK_TAG}" \
|
||
"${IMAGE_NAME}:${IMAGE_TAG}"
|
||
info "retag 完成"
|
||
else
|
||
info "skip 3/5(非 rollback mode)"
|
||
fi
|
||
|
||
step "4/5 Deploy via docker compose"
|
||
|
||
# 假設 stage host 上 /opt/visiona/ 已有 docker-compose.stage.yml + .env.stage
|
||
# (首次部署需手動 scp 上去;見 docs/STAGE-DEPLOY.md)
|
||
DOCKER_HOST="$DOCKER_REMOTE" docker compose \
|
||
-f "$COMPOSE_FILE" \
|
||
--project-directory "$PROJECT_ROOT" \
|
||
-p visiona-stage \
|
||
up -d --remove-orphans
|
||
|
||
info "container 已啟動,等 healthcheck(最多 60s)..."
|
||
|
||
# ──────────── verify ────────────
|
||
step "5/5 Verify"
|
||
|
||
# 5a. container 狀態
|
||
DOCKER_HOST="$DOCKER_REMOTE" docker ps --filter name=visiona --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
|
||
|
||
# 5b. 等 healthcheck 變 healthy(最多 60 秒)
|
||
deadline=$(( $(date +%s) + 60 ))
|
||
healthy=false
|
||
while [ "$(date +%s)" -lt "$deadline" ]; do
|
||
status=$(DOCKER_HOST="$DOCKER_REMOTE" docker inspect --format='{{.State.Health.Status}}' visiona 2>/dev/null || echo "unknown")
|
||
if [ "$status" = "healthy" ]; then
|
||
healthy=true
|
||
break
|
||
fi
|
||
sleep 3
|
||
done
|
||
|
||
if [ "$healthy" = true ]; then
|
||
info "container healthy ✓"
|
||
else
|
||
warn "container healthcheck 超時(status=$status)"
|
||
warn "查看 log: DOCKER_HOST=$DOCKER_REMOTE docker logs visiona --tail 100"
|
||
fi
|
||
|
||
# 5c. 對外 URL probe(公司 host nginx → container)
|
||
if curl -fsS --max-time 10 "$HEALTHZ_URL" >/dev/null 2>&1; then
|
||
info "對外 healthz ✓ ($HEALTHZ_URL)"
|
||
else
|
||
warn "對外 healthz 失敗 — 可能公司 host nginx 還沒接好 :9527 → container"
|
||
warn "請檢查公司 IT 那邊 stage-9527.innovedus.com 反代設定"
|
||
fi
|
||
|
||
echo ""
|
||
echo -e "${GREEN}=== Deploy 完成 ===${NC}"
|
||
echo ""
|
||
info "URL : https://${STAGE_DOMAIN}:${STAGE_PORT}/"
|
||
info "Logs: DOCKER_HOST=$DOCKER_REMOTE docker logs -f visiona"
|
||
echo ""
|
||
hint "Rollback hint:"
|
||
hint " bash scripts/deploy-stage.sh --rollback ${TIMESTAMPED_TAG:-<舊 tag>}"
|
||
hint ""
|
||
hint "緊急救回 POC(visionA 部 fail 時):"
|
||
hint " DOCKER_HOST=$DOCKER_REMOTE docker stop visiona"
|
||
hint " DOCKER_HOST=$DOCKER_REMOTE docker start edge-ai-platform"
|
||
echo ""
|