chore(stage): 新增 v2 deploy 流程(remote build via DOCKER_HOST)

v1 (deploy-stage.sh) 走 docker save | gzip | docker load 模式,需要把 81MB
tarball 一次性 POST 到 stage docker daemon /images/load API。5/4 / 5/9 兩次
驗證 VPN 下 docker daemon 對單一大 POST hang(卡 30+ 分鐘 / i/o timeout),
公司網段直連才可靠。

v2 仿 edge-ai-platform/scripts/deploy-docker.sh 改用 DOCKER_HOST=stage
docker build — multi-stage build 完全在 stage daemon 上執行:
- 跨網路只傳 build context(~44 MB streaming,VPN 友善)
- alpine base / nodejs / go mod / pnpm install 都由 stage daemon 自己 pull
- layer cache 留在 stage daemon,後續 incremental build 更快
- 5/9 VPN 下實測 work:first build ~3min、redeploy(layer cache) ~10s

連帶修:
- pnpm-workspace.yaml: 加 onlyBuiltDependencies (sharp / unrs-resolver /
  @tailwindcss/oxide / esbuild) — pnpm 10 預設拒跑依賴 build script、
  乾淨環境第一次 install 撞 ERR_PNPM_IGNORED_BUILDS
- package.json: 加 packageManager: pnpm@10.30.1 — 鎖 pnpm 版本,corepack
  在 stage daemon 第一次跑時不會拉到最新 pnpm 11(行為差異)
- Dockerfile.stage: COPY pnpm-workspace.yaml 進 builder context、否則
  容器內 install 看不到 trust list

v1 (deploy-stage.sh) 保留作為公司網段直連備援;v2 是 VPN / 預設模式。
This commit is contained in:
jim800121chen 2026-05-11 10:35:21 +08:00
parent fb7da5d180
commit 700b7b08ba
4 changed files with 242 additions and 1 deletions

View File

@ -41,7 +41,9 @@ RUN corepack enable
WORKDIR /src
# 先 COPY lockfile 讓依賴 layer 可以被 cache。
COPY visionA-frontend/package.json visionA-frontend/pnpm-lock.yaml ./
# pnpm-workspace.yaml 含 onlyBuiltDependencies trust listpnpm 10 不執行依賴 build script
# 必須明確 allow否則乾淨環境第一次 install 撞 ERR_PNPM_IGNORED_BUILDS
COPY visionA-frontend/package.json visionA-frontend/pnpm-lock.yaml visionA-frontend/pnpm-workspace.yaml ./
RUN pnpm install --frozen-lockfile
# 複製其餘原始碼後 build。

213
scripts/deploy-stage-v2.sh Executable file
View File

@ -0,0 +1,213 @@
#!/usr/bin/env bash
#
# visionA — Stage Deploy v2remote build 模式,仿 edge-ai-platform/scripts/deploy-docker.sh
#
# 為什麼有 v2
# v1deploy-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 MBstreaming 上傳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-flightdocker buildx、git status
# 1. DOCKER_HOST=stage docker buildx buildmulti-stage 全在 stage 跑)
# 2. DOCKER_HOST=stage docker compose up -d同 v1
# 3. Verifycontainer 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 upimage 已 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 下小流量 OKversion / 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 連線 OKserver 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)"
# 用 buildxbuildx 預設 builder 是 default = remote daemon driver
# 不需要 --load / --pushbuild 完 image 直接留在 stage daemon
# 不需要 --platformDOCKER_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 hintDOCKER_HOST=$DOCKER_REMOTE docker tag visiona:stage-<old-tag> visiona:stage && bash scripts/deploy-stage-v2.sh --skip-build"
echo ""

View File

@ -3,6 +3,7 @@
"version": "0.1.0",
"private": true,
"description": "visionA Cloud 前端Phase 0 雛形骨架)",
"packageManager": "pnpm@10.30.1",
"scripts": {
"dev": "next dev",
"build": "next build",

View File

@ -0,0 +1,25 @@
# pnpm 10 設定(不是 monorepo workspace、純粹用來放 onlyBuiltDependencies
#
# 為什麼有此檔:
# pnpm 10 預設不執行依賴的 install 階段 build scriptpostinstall / preinstall
# 作為 supply-chain 攻擊防護。需要明確 allow-list 哪些 package 可以跑 build script。
#
# 本機 install 不會撞是因為 ~/.config/pnpm 或 node_modules 殘留 trust
# 但乾淨環境(如 stage docker build / CI第一次 pnpm install 就會 ERR_PNPM_IGNORED_BUILDS。
#
# 為什麼這幾個 package 需要:
# - sharp@0.34.5 — next.js image optimization、native libvips bindinginstall 時 prebuild download
# - unrs-resolver@1.11.1 — eslint-config-next 用的 rust binary resolver
# - @tailwindcss/oxide — tailwindcss 4 的 rust 引擎
# - esbuild — vitest / next 內部建置工具install 時下載對應平台 binary
#
# 如何加新項目:
# 1. CI 撞 ERR_PNPM_IGNORED_BUILDS 時看訊息列出的 package 名
# 2. 確認該 package 來源可信npmjs / github 看 popularity
# 3. 加到下方 onlyBuiltDependencies 並 commit
onlyBuiltDependencies:
- sharp
- unrs-resolver
- "@tailwindcss/oxide"
- esbuild