chore(deploy): containerize server deployment

This commit is contained in:
2026-07-06 11:31:55 +08:00
parent 473f6a3726
commit 6014ac845d
6 changed files with 285 additions and 165 deletions
+104 -121
View File
@@ -1,19 +1,21 @@
#!/bin/sh
# Post-receive hook: push 后自动 build + start
# Post-receive hook: push 后自动构建 Docker 镜像并重启容器
# pnpm 环境变量
# pnpm / node 环境仍保留,便于服务器缺少 Docker 时做人工排查。
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
# 全局变量统一声明。具体值由各步骤函数写入。
REPO_TOPLEVEL=""
LOG_FILE=""
CURRENT_BRANCH=""
DEPLOY_ENV=""
ENV_FILE=""
START_PORT=""
BUILD_EXIT=0
STOP_WAIT_SECONDS=3
STOP_VERIFY_RETRIES=3
IMAGE_TAG=""
CONTAINER_NAME=""
COMPOSE_PROJECT_NAME=""
COMPOSE_EXIT=0
resolve_repo_context() {
GIT_DIR_ABS="$(git rev-parse --absolute-git-dir 2>/dev/null)"
@@ -44,8 +46,8 @@ write_metadata_header() {
}
sync_worktree() {
echo "=== sync_worktree: git reset --hard HEAD @ $(date +%Y-%m-%dT%H:%M:%S%z) ==="
git reset --hard HEAD
echo "=== sync_worktree: git reset --hard HEAD @ $(date +%Y-%m-%dT%H:%M:%S%z) ===" >> "$LOG_FILE"
git reset --hard HEAD >> "$LOG_FILE" 2>&1
}
copy_env_by_branch() {
@@ -54,67 +56,86 @@ copy_env_by_branch() {
case "$CURRENT_BRANCH" in
main)
cp -f env-example/.env.production.example .env.production
ENV_FILE=".env.production"
DEPLOY_ENV="prod"
START_PORT=9185
echo "=== env: copied .env.production (main → prod) ===" >> "$LOG_FILE"
;;
test)
cp -f env-example/.env.local.example .env.local
ENV_FILE=".env.local"
DEPLOY_ENV="test"
START_PORT=9135
echo "=== env: copied .env.local (test → test) ===" >> "$LOG_FILE"
;;
dev)
cp -f env-example/.env.development.example .env.local
ENV_FILE=".env.local"
DEPLOY_ENV="dev"
START_PORT=9135
echo "=== env: copied .env.local (dev → dev) ===" >> "$LOG_FILE"
;;
*)
echo "=== env: UNKNOWN branch '$CURRENT_BRANCH', skip env copy ===" >> "$LOG_FILE"
ENV_FILE=".env.local"
DEPLOY_ENV="$CURRENT_BRANCH"
START_PORT=9135
echo "=== env: UNKNOWN branch '$CURRENT_BRANCH', use defaults ===" >> "$LOG_FILE"
;;
esac
}
# ============================================================
# 4. select_port_by_branch —— 根据分支选择 next start 端口
# main 分支 → 9185
# test 分支 → 9135
# dev 分支 → 9135
# ============================================================
select_port_by_branch() {
case "$CURRENT_BRANCH" in
main)
START_PORT=9185
;;
test|dev)
START_PORT=9135
;;
*)
START_PORT=9135
;;
esac
export START_PORT
echo "=== start port: $START_PORT ===" >> "$LOG_FILE"
prepare_compose_env() {
SHORT_SHA="$(git rev-parse --short HEAD)"
IMAGE_TAG="cozsweet-web:${DEPLOY_ENV}-${SHORT_SHA}"
CONTAINER_NAME="cozsweet-web-${DEPLOY_ENV}"
COMPOSE_PROJECT_NAME="cozsweet-web-${DEPLOY_ENV}"
export COZSWEET_IMAGE="$IMAGE_TAG"
export COZSWEET_CONTAINER_NAME="$CONTAINER_NAME"
export COZSWEET_ENV_FILE="$ENV_FILE"
export COZSWEET_HOST_PORT="$START_PORT"
export COMPOSE_PROJECT_NAME
export DOCKER_BUILDKIT=1
export COMPOSE_DOCKER_CLI_BUILD=1
{
echo "=== deploy env: $DEPLOY_ENV ==="
echo "=== env file: $COZSWEET_ENV_FILE ==="
echo "=== image: $COZSWEET_IMAGE ==="
echo "=== container: $COZSWEET_CONTAINER_NAME ==="
echo "=== host port: $COZSWEET_HOST_PORT ==="
echo "=== compose project: $COMPOSE_PROJECT_NAME ==="
echo ""
} >> "$LOG_FILE"
}
# ============================================================
# 5. run_build —— 跑 `pnpm install` + `pnpm run build`,输出丢,捕获退出码写日志
# 返回:build 退出码(main 用 if ! run_build 决定是否继续)
# ============================================================
run_build() {
echo "=== build START @ $(date +%Y-%m-%dT%H:%M:%S%z) ===" >> "$LOG_FILE"
pnpm run build:deploy > /dev/null 2>&1
BUILD_EXIT=$?
echo "=== build EXIT_CODE=$BUILD_EXIT @ $(date +%Y-%m-%dT%H:%M:%S%z) ===" >> "$LOG_FILE"
docker_compose() {
if docker compose version >/dev/null 2>&1; then
docker compose "$@"
return $?
fi
if command -v docker-compose >/dev/null 2>&1; then
docker-compose "$@"
return $?
fi
echo "=== docker compose command not found ===" >> "$LOG_FILE"
return 127
}
run_container_build() {
echo "=== docker build START @ $(date +%Y-%m-%dT%H:%M:%S%z) ===" >> "$LOG_FILE"
docker_compose build web >> "$LOG_FILE" 2>&1
COMPOSE_EXIT=$?
echo "=== docker build EXIT_CODE=$COMPOSE_EXIT @ $(date +%Y-%m-%dT%H:%M:%S%z) ===" >> "$LOG_FILE"
echo "" >> "$LOG_FILE"
return $BUILD_EXIT
return $COMPOSE_EXIT
}
# ============================================================
# 6. stop_existing_next —— 强杀当前分支端口上的旧进程
# 1) 使用 ss 查找占用 START_PORT 的进程
# 2) 直接 SIGKILL,避免旧服务 drain 太久导致新服务无法绑定端口
# 3) 等待 STOP_WAIT_SECONDS,让系统释放端口
# 4) 循环复查端口占用并再次强杀残留
# ============================================================
find_port_pids() {
if ! command -v ss >/dev/null 2>&1; then
echo "=== ss command not found; cannot inspect port $START_PORT ===" >> "$LOG_FILE"
echo "=== ss command not found; cannot inspect legacy port $START_PORT ===" >> "$LOG_FILE"
return 0
fi
@@ -124,99 +145,61 @@ find_port_pids() {
| sort -u
}
force_kill_pids() {
PIDS_TO_KILL="$1"
if [ -z "$PIDS_TO_KILL" ]; then
container_exists() {
docker ps -a --format '{{.Names}}' 2>/dev/null | grep -Fx "$CONTAINER_NAME" >/dev/null 2>&1
}
stop_legacy_next_if_needed() {
if container_exists; then
echo "=== existing container found; skip legacy port cleanup ===" >> "$LOG_FILE"
return 0
fi
echo "=== force kill PIDS: $PIDS_TO_KILL ===" >> "$LOG_FILE"
kill -9 $PIDS_TO_KILL 2>/dev/null || true
}
wait_for_port_release() {
RETRY=0
while [ "$RETRY" -lt "$STOP_VERIFY_RETRIES" ]; do
REMAINING_PIDS="$(find_port_pids)"
if [ -z "$REMAINING_PIDS" ]; then
echo "=== port $START_PORT released ===" >> "$LOG_FILE"
return 0
fi
echo "=== port $START_PORT still busy, retry=$RETRY, pids=$REMAINING_PIDS ===" >> "$LOG_FILE"
force_kill_pids "$REMAINING_PIDS"
sleep "$STOP_WAIT_SECONDS"
RETRY=$((RETRY + 1))
done
REMAINING_PIDS="$(find_port_pids)"
if [ -n "$REMAINING_PIDS" ]; then
echo "=== stop FAILED: port $START_PORT still occupied by PIDS: $REMAINING_PIDS ===" >> "$LOG_FILE"
return 1
fi
echo "=== port $START_PORT released ===" >> "$LOG_FILE"
return 0
}
stop_existing_next() {
echo "=== stop existing next on port $START_PORT @ $(date +%Y-%m-%dT%H:%M:%S%z) ===" >> "$LOG_FILE"
PIDS="$(find_port_pids)"
if [ -n "$PIDS" ]; then
force_kill_pids "$PIDS"
else
echo "=== no process found on port $START_PORT ===" >> "$LOG_FILE"
if [ -z "$PIDS" ]; then
echo "=== no legacy process found on port $START_PORT ===" >> "$LOG_FILE"
return 0
fi
sleep "$STOP_WAIT_SECONDS"
echo "=== first container deploy: kill legacy PIDS on port $START_PORT: $PIDS ===" >> "$LOG_FILE"
kill -9 $PIDS 2>/dev/null || true
sleep 3
}
if ! wait_for_port_release; then
echo "=== stop ABORT @ $(date +%Y-%m-%dT%H:%M:%S%z) ===" >> "$LOG_FILE"
echo "" >> "$LOG_FILE"
return 1
fi
echo "=== stop DONE @ $(date +%Y-%m-%dT%H:%M:%S%z) ===" >> "$LOG_FILE"
launch_container() {
echo "=== docker compose up START @ $(date +%Y-%m-%dT%H:%M:%S%z) ===" >> "$LOG_FILE"
stop_legacy_next_if_needed
docker_compose up -d --remove-orphans web >> "$LOG_FILE" 2>&1
COMPOSE_EXIT=$?
echo "=== docker compose up EXIT_CODE=$COMPOSE_EXIT @ $(date +%Y-%m-%dT%H:%M:%S%z) ===" >> "$LOG_FILE"
echo "" >> "$LOG_FILE"
return 0
return $COMPOSE_EXIT
}
launch_next() {
echo "=== start LAUNCH port=$START_PORT @ $(date +%Y-%m-%dT%H:%M:%S%z) ===" >> "$LOG_FILE"
PORT="$START_PORT" nohup pnpm run start >> "$LOG_FILE" 2>&1 &
START_PID=$!
echo "=== start PID=$START_PID @ $(date +%Y-%m-%dT%H:%M:%S%z) ===" >> "$LOG_FILE"
}
# ============================================================
# 8. log_abort —— build 失败时写结束标记(不输出 START LAUNCH
# ============================================================
log_abort() {
echo "=== ABORTED @ $(date +%Y-%m-%dT%H:%M:%S%z) (build failed, start skipped) ===" >> "$LOG_FILE"
echo "=== ABORTED @ $(date +%Y-%m-%dT%H:%M:%S%z) ($1) ===" >> "$LOG_FILE"
}
# ============================================================
# main —— 调度所有步骤
# ============================================================
main() {
resolve_repo_context
init_log_file
write_metadata_header # 先覆盖写元信息(这样 env copy 日志能保留在后面)
write_metadata_header
sync_worktree # 先同步工作树到新 HEAD(与 git config denyCurrentBranch 配合)
copy_env_by_branch # 追加 env copy 日志
sync_worktree
copy_env_by_branch
prepare_compose_env
select_port_by_branch # 根据当前分支决定 next start 端口
if ! run_build; then
log_abort
exit $BUILD_EXIT # build 失败 → 不启 start(避免用旧 .next 跑新代码)
if ! run_container_build; then
log_abort "docker build failed, old container kept running"
exit $COMPOSE_EXIT
fi
if ! stop_existing_next; then
exit 1 # 端口未释放 → 不启动新服务,避免 EADDRINUSE
if ! launch_container; then
log_abort "docker compose up failed"
exit $COMPOSE_EXIT
fi
launch_next # 再启新
echo "=== DEPLOY DONE @ $(date +%Y-%m-%dT%H:%M:%S%z) ===" >> "$LOG_FILE"
}
main "$@"