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 / 預設模式。
214 lines
8.1 KiB
Bash
Executable File
214 lines
8.1 KiB
Bash
Executable File
#!/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 ""
|