diff --git a/.githooks/post-receive b/.githooks/post-receive index 81d0235d..5dc221ef 100755 --- a/.githooks/post-receive +++ b/.githooks/post-receive @@ -1,11 +1,6 @@ #!/bin/sh # Post-receive hook: push 后自动构建 Docker 镜像并重启容器 -# pnpm / node 环境仍保留,便于服务器缺少 Docker 时做人工排查。 -export NVM_DIR="$HOME/.nvm" -[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" -[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" - REPO_TOPLEVEL="" LOG_FILE="" CURRENT_BRANCH="" @@ -133,42 +128,8 @@ run_container_build() { return $COMPOSE_EXIT } -find_port_pids() { - if ! command -v ss >/dev/null 2>&1; then - echo "=== ss command not found; cannot inspect legacy port $START_PORT ===" >> "$LOG_FILE" - return 0 - fi - - ss -tulnp 2>/dev/null \ - | awk -v port=":$START_PORT" '$5 ~ port "$" { print }' \ - | sed -n 's/.*pid=\([0-9][0-9]*\).*/\1/p' \ - | sort -u -} - -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 - - PIDS="$(find_port_pids)" - if [ -z "$PIDS" ]; then - echo "=== no legacy process found on port $START_PORT ===" >> "$LOG_FILE" - return 0 - fi - - echo "=== first container deploy: kill legacy PIDS on port $START_PORT: $PIDS ===" >> "$LOG_FILE" - kill -9 $PIDS 2>/dev/null || true - sleep 3 -} - 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" diff --git a/docs/auto-build-restart.md b/docs/auto-build-restart.md index cfc7f2e0..31d30248 100644 --- a/docs/auto-build-restart.md +++ b/docs/auto-build-restart.md @@ -66,18 +66,16 @@ production root@43.106.13.130:/root/cozsweet-repos/main 服务器收到 push 后,`post-receive` hook 会自动执行以下步骤: -1. 初始化 Node / pnpm 环境。 -2. 解析当前仓库目录,进入服务端工作树。 -3. 初始化日志文件:`logs/post-receive.log`。 -4. 覆盖写入本次执行元信息,包括时间、分支、commit、cwd。 -5. 执行 `git reset --hard HEAD`,让服务端工作树同步到刚推送的最新 commit。 -6. 根据当前分支复制环境变量文件。 -7. 根据当前分支选择 Next.js 启动端口。 -8. 根据分支导出 Docker Compose 所需变量,包括镜像名、容器名、env 文件和端口。 -9. 执行 `docker compose build web` 构建环境专属镜像。 -10. 如果镜像构建失败,写入失败日志并中止,旧容器继续运行。 -11. 如果镜像构建成功,执行 `docker compose up -d --remove-orphans web` 重建并启动容器。 -12. 第一次从裸机 `next start` 迁移到容器时,如果目标端口上没有同名容器但存在旧进程,会清理旧进程后再启动容器。 +1. 解析当前仓库目录,进入服务端工作树。 +2. 初始化日志文件:`logs/post-receive.log`。 +3. 覆盖写入本次执行元信息,包括时间、分支、commit、cwd。 +4. 执行 `git reset --hard HEAD`,让服务端工作树同步到刚推送的最新 commit。 +5. 根据当前分支复制环境变量文件。 +6. 根据当前分支选择 Next.js 启动端口。 +7. 根据分支导出 Docker Compose 所需变量,包括镜像名、容器名、env 文件和端口。 +8. 执行 `docker compose build web` 构建环境专属镜像。 +9. 如果镜像构建失败,写入失败日志并中止,旧容器继续运行。 +10. 如果镜像构建成功,执行 `docker compose up -d --remove-orphans web` 重建并启动容器。 ## 分支、环境变量、端口映射 @@ -94,6 +92,41 @@ production root@43.106.13.130:/root/cozsweet-repos/main 本地部署脚本清除 Cloudflare CDN 缓存时,也会按当前分支读取 env:`main` 优先读取 `.env.production`,其他分支优先读取 `.env.local`。如果优先文件不存在或缺少 `CF_ZONE_ID` / `CF_API_TOKEN`,会尝试读取另一个文件作为兜底。 +## 生产容器化启用与验证 + +生产环境容器化通过 `main` 分支触发。发布入口为: + +```bash +./scripts/release/release_web.sh +``` + +发布完成后,可在生产服务器工作树中验证: + +```bash +cd /root/cozsweet-repos/main + +# 确认生产工作树已更新到 main 最新 commit +git branch --show-current +git rev-parse --short HEAD + +# 查看生产容器 +docker ps --filter 'name=cozsweet-web-prod' +docker inspect cozsweet-web-prod --format '{{.State.Health.Status}}' + +# 验证生产宿主机端口和 PWA service worker +curl -I http://127.0.0.1:9185/ +curl -I http://127.0.0.1:9185/serwist/sw.js +``` + +如果本次发布同时更新了 `.githooks/post-receive` 本身,而服务器这次 push 仍由旧 hook 执行,可在生产工作树中手动触发一次新 hook: + +```bash +cd /root/cozsweet-repos/main +./.githooks/post-receive +``` + +后续生产发布会自动使用新的容器化 hook。 + ## 容器构建机制 Docker 构建使用 Next.js standalone 输出: @@ -102,8 +135,9 @@ Docker 构建使用 Next.js standalone 输出: 2. `Dockerfile` 使用 multi-stage build。 3. build 阶段通过 Docker BuildKit secret 挂载 `.env.local` 或 `.env.production`。 4. build 完成后删除临时 env 文件,避免 env 文件被复制到最终镜像。 -5. runtime 阶段只复制 `.next/standalone`、`.next/static` 和 `public`。 -6. 容器通过 `node server.js` 启动,不再依赖完整 `node_modules` 或 `next start`。 +5. runtime 阶段复制 `.next/standalone`、`.next/static`、`public` 和生产依赖 `node_modules`。 +6. 生产依赖用于支持 Serwist 在运行时生成 `/serwist/sw.js`,避免手动修复 pnpm symlink。 +7. 容器通过 `node server.js` 启动,不再使用 `next start`。 注意:`NEXT_PUBLIC_*` 变量会在 `next build` 时固化到前端 bundle,因此测试环境和生产环境必须分别构建镜像,不能共用同一个镜像。 @@ -122,8 +156,7 @@ docker compose up -d --remove-orphans web 4. Compose 会使用新镜像重建服务容器。 5. 容器配置了 `restart: unless-stopped`,容器异常退出后由 Docker 自动拉起。 6. 容器内固定监听 `3000`,宿主机端口按分支映射为 `9135` 或 `9185`。 - -首次迁移时,如果宿主机端口仍被旧的裸机 `next start` 进程占用,hook 会在构建成功后、启动容器前清理旧进程。后续如果同名容器已经存在,则不再手动清理端口,由 Docker Compose 管理容器替换。 +7. hook 不再主动清理宿主机端口进程,端口和容器生命周期交给 Docker Compose 管理。 ## 日志与排查 @@ -184,6 +217,7 @@ ABORTED ... (docker compose up failed) 5. 构建失败时不会重启容器,旧容器继续运行。 6. 服务启动由 Docker Compose 管理,不再使用 `nohup pnpm run start`。 7. 服务器需要安装 Docker,并支持 `docker compose` 或旧版 `docker-compose` 命令。 +8. 测试环境和生产环境都通过同一套 `post-receive` 容器化流程部署,差异仅由分支、环境变量文件和端口决定。 ## 后续优化建议 diff --git a/scripts/deploy/_deploy_lib.sh b/scripts/deploy/_deploy_lib.sh index 683f7bc8..fd02fad6 100755 --- a/scripts/deploy/_deploy_lib.sh +++ b/scripts/deploy/_deploy_lib.sh @@ -121,11 +121,11 @@ purge_cdn_cache() { source_env_file "$primary_env" || true - if [ -z "$CF_ZONE_ID" ] || [ -z "$CF_API_TOKEN" ]; then + if [ -z "${CF_ZONE_ID:-}" ] || [ -z "${CF_API_TOKEN:-}" ]; then source_env_file "$fallback_env" || true fi - if [ -z "$CF_ZONE_ID" ] || [ -z "$CF_API_TOKEN" ]; then + if [ -z "${CF_ZONE_ID:-}" ] || [ -z "${CF_API_TOKEN:-}" ]; then echo "错误: Cloudflare 配置不完整(CF_ZONE_ID / CF_API_TOKEN),跳过缓存清除" return 1 fi diff --git a/scripts/deploy/deploy_web.sh b/scripts/deploy/deploy_web.sh index ba057f26..fca5edea 100755 --- a/scripts/deploy/deploy_web.sh +++ b/scripts/deploy/deploy_web.sh @@ -4,6 +4,8 @@ # 原始 Dart: scripts/deploy/deploy_web.sh # ========================================== +set -euo pipefail + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/_deploy_lib.sh" diff --git a/scripts/deploy/deploy_web_test.sh b/scripts/deploy/deploy_web_test.sh index 90181b1b..3a28a510 100755 --- a/scripts/deploy/deploy_web_test.sh +++ b/scripts/deploy/deploy_web_test.sh @@ -4,6 +4,8 @@ # 原始 Dart: scripts/deploy/deploy_web_test.sh # ========================================== +set -euo pipefail + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/_deploy_lib.sh" diff --git a/scripts/release/release_web.sh b/scripts/release/release_web.sh index 268a6502..a437da3a 100755 --- a/scripts/release/release_web.sh +++ b/scripts/release/release_web.sh @@ -1,5 +1,16 @@ #!/bin/bash +set -euo pipefail + +unset_git_hook_env() { + local var + while IFS= read -r var; do + [ -n "$var" ] && unset "$var" + done < <(git rev-parse --local-env-vars 2>/dev/null || true) +} + +unset_git_hook_env + # Main 分支 worktree 路径 WORKTREE_PATH="/Users/chase/Documents/frontend/cozsweet-frontend-nextjs.worktrees/cozsweet-nextjs-main"