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:
parent
fb7da5d180
commit
700b7b08ba
@ -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 list(pnpm 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
213
scripts/deploy-stage-v2.sh
Executable file
@ -0,0 +1,213 @@
|
||||
#!/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-<old-tag> visiona:stage && bash scripts/deploy-stage-v2.sh --skip-build"
|
||||
echo ""
|
||||
@ -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",
|
||||
|
||||
25
visionA-frontend/pnpm-workspace.yaml
Normal file
25
visionA-frontend/pnpm-workspace.yaml
Normal file
@ -0,0 +1,25 @@
|
||||
# pnpm 10 設定(不是 monorepo workspace、純粹用來放 onlyBuiltDependencies)
|
||||
#
|
||||
# 為什麼有此檔:
|
||||
# pnpm 10 預設不執行依賴的 install 階段 build script(postinstall / 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 binding,install 時 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
|
||||
Loading…
x
Reference in New Issue
Block a user