#!/usr/bin/env bash # # visionA — Stage Deploy v2(remote build 模式,仿 edge-ai-platform/scripts/deploy-docker.sh) # # 為什麼有 v2: # v1(deploy-stage.sh)走 docker save | gzip | docker load 模式,需要把 81MB # tarball 一次性 POST 到 stage docker daemon `/images/load`。經驗證 VPN 下 # docker daemon 對單一大 POST 有 read timeout / hang 問題(5/4 卡 30+ 分、 # 5/9 i/o timeout / 也卡 30 分),公司網段直連才可靠。 # # v2 改用 `DOCKER_HOST=stage docker buildx build .` — multi-stage build # 完全在 stage daemon 上執行: # - 跨網路只傳 build context(~44 MB,streaming 上傳,VPN 友善) # - alpine base / nodejs apk / go mod download / pnpm install 都是 stage # daemon 自己 pull / 抓(公司內網 → docker hub / npm registry) # - layer cache 留在 stage daemon、後續 deploy 只重 build 改動的 layer # # 流程(仿 edge-ai-platform deploy-docker.sh): # 0. Pre-flight:docker buildx、git status # 1. DOCKER_HOST=stage docker buildx build(multi-stage 全在 stage 跑) # 2. DOCKER_HOST=stage docker compose up -d(同 v1) # 3. Verify:container status + healthz # # 跟 v1 (deploy-stage.sh) 的差別: # - v1:本機 buildx --load → save | gzip | load → compose up # - v2:直接 DOCKER_HOST=stage buildx build → compose up(沒 save/load 步驟) # # 用法: # bash scripts/deploy-stage-v2.sh # full deploy # bash scripts/deploy-stage-v2.sh --skip-build # 不重 build,只 compose up # bash scripts/deploy-stage-v2.sh --no-deploy # build 完不 compose up(驗證 build) # bash scripts/deploy-stage-v2.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 設定 ──────────── DOCKER_REMOTE="${DOCKER_HOST:-tcp://192.168.0.130:2375}" IMAGE_NAME="visiona" IMAGE_TAG="stage" IMAGE_REF="${IMAGE_NAME}:${IMAGE_TAG}" 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 v2 (remote build 模式) Usage: bash scripts/deploy-stage-v2.sh [OPTIONS] Options: --skip-build Skip build, just compose up(image 已 build 過) --no-deploy Build only, do not compose up --help Show this help Environment: DOCKER_HOST Remote Docker daemon (default: tcp://192.168.0.130:2375) Examples: bash scripts/deploy-stage-v2.sh bash scripts/deploy-stage-v2.sh --skip-build bash scripts/deploy-stage-v2.sh --no-deploy After deployment: https://stage-9527.innovedus.com:9527/ HELP } # ──────────── parse args ──────────── SKIP_BUILD=false NO_DEPLOY=false while [ $# -gt 0 ]; do case "$1" in --skip-build) SKIP_BUILD=true; shift ;; --no-deploy) NO_DEPLOY=true; shift ;; --help|-h) show_help; exit 0 ;; *) error "Unknown option: $1 (use --help)" ;; esac done # ──────────── pre-flight ──────────── step "0/3 Pre-flight checks" command -v docker >/dev/null 2>&1 || error "docker 未安裝" [ -f "$DOCKERFILE" ] || error "找不到 Dockerfile.stage:$DOCKERFILE" [ -f "$COMPOSE_FILE" ] || error "找不到 docker-compose.stage.yml:$COMPOSE_FILE" # 確認 stage daemon 連得上(v2 必須能連、v1 也是) info "確認 stage docker daemon 連線:$DOCKER_REMOTE" if ! DOCKER_HOST="$DOCKER_REMOTE" docker version --format '{{.Server.Version}}' >/dev/null 2>&1; then error "無法連到 stage docker daemon @ $DOCKER_REMOTE 提示: - 公司內網需直連 192.168.0.130:2375 - VPN 下小流量 OK(version / images),但大流量會卡 - v2 模式跨網路傳 ~44 MB build context,比 v1 友善但仍非 100% 保證" fi STAGE_VERSION=$(DOCKER_HOST="$DOCKER_REMOTE" docker version --format '{{.Server.Version}}' 2>/dev/null || echo unknown) info "stage daemon 連線 OK(server version: $STAGE_VERSION)" # git 狀態(warn only) cd "$PROJECT_ROOT" GIT_SHA="$(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}" if ! git diff-index --quiet HEAD -- 2>/dev/null; then warn "git working tree 有未 commit 的變更(stage build 仍會繼續)" fi info "Project root : $PROJECT_ROOT" info "Docker remote : $DOCKER_REMOTE" info "Image : $IMAGE_REF" info "Timestamp tag : ${IMAGE_NAME}:${TIMESTAMPED_TAG}" info "Git SHA : $GIT_SHA" # ──────────── build (在 stage daemon 上) ──────────── if [ "$SKIP_BUILD" = false ]; then step "1/3 Remote build (DOCKER_HOST=stage docker buildx build)" info "build 完全在 stage daemon 上執行;只跨網路傳 build context (~44 MB streaming)" # 用 buildx(buildx 預設 builder 是 default = remote daemon driver) # 不需要 --load / --push:build 完 image 直接留在 stage daemon # 不需要 --platform:DOCKER_HOST 指向 stage daemon、stage 是 linux/amd64、native build cd "$PROJECT_ROOT" DOCKER_HOST="$DOCKER_REMOTE" docker build \ -f "$DOCKERFILE" \ -t "${IMAGE_NAME}:${IMAGE_TAG}" \ -t "${IMAGE_NAME}:${TIMESTAMPED_TAG}" \ . info "build 完成 — stage daemon 上已有 ${IMAGE_NAME}:${IMAGE_TAG} + ${IMAGE_NAME}:${TIMESTAMPED_TAG}" else info "skip 1/3(--skip-build)— 假設 ${IMAGE_NAME}:${IMAGE_TAG} 已存在於 stage daemon" fi # ──────────── deploy ──────────── if [ "$NO_DEPLOY" = true ]; then info "skip 2/3, 3/3(--no-deploy)" info "下一步:執行 bash scripts/deploy-stage-v2.sh --skip-build 完成 deploy" exit 0 fi step "2/3 Deploy via docker compose" 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 "3/3 Verify" DOCKER_HOST="$DOCKER_REMOTE" docker ps --filter name=visiona --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" deadline=$(( $(date +%s) + 60 )) healthy=false status=unknown 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)— 注意 5/1 已知 healthcheck 從 container 內被 nginx server_name 擋成 444 是 false negative,外部 healthz 才是真實狀態" fi if curl -fsS --max-time 10 "$HEALTHZ_URL" >/dev/null 2>&1; then info "對外 healthz ✓ ($HEALTHZ_URL)" else warn "對外 healthz 失敗:$HEALTHZ_URL" warn "查看 log: DOCKER_HOST=$DOCKER_REMOTE docker logs visiona --tail 100" fi echo "" echo -e "${GREEN}=== Deploy v2 完成 ===${NC}" echo "" info "URL : https://${STAGE_DOMAIN}:${STAGE_PORT}/" info "Logs: DOCKER_HOST=$DOCKER_REMOTE docker logs -f visiona" echo "" hint "Rollback hint:DOCKER_HOST=$DOCKER_REMOTE docker tag visiona:stage- visiona:stage && bash scripts/deploy-stage-v2.sh --skip-build" echo ""