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
|
WORKDIR /src
|
||||||
|
|
||||||
# 先 COPY lockfile 讓依賴 layer 可以被 cache。
|
# 先 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
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
# 複製其餘原始碼後 build。
|
# 複製其餘原始碼後 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",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "visionA Cloud 前端(Phase 0 雛形骨架)",
|
"description": "visionA Cloud 前端(Phase 0 雛形骨架)",
|
||||||
|
"packageManager": "pnpm@10.30.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"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