结论:用 post-receive 钩子 + git_work_tree 是最稳妥的自动部署方式,因裸仓库无工作目录,post-update 会报“fatal: this operation must be run in a work tree”错误,而 post-receive 可安全指定 --work-tree 和 --git-dir 并读取标准输入中的 ref 信息,支持分支判断且兼容 git 2.30+。

直接说结论:用 post-receive 钩子 + GIT_WORK_TREE 是最稳妥的自动部署方式,别碰 post-update 或 checkout 在裸仓库里直接操作——它会失败,且不报明确错误。
为什么必须用 post-receive 而不是 post-update
裸仓库(bare repo)没有工作目录,post-update 默认执行时仍处于 .git 内部,git checkout 会报错 fatal: this operation must be run in a work tree。而 post-receive 是唯一在接收推送数据后、能安全控制 --work-tree 和 --git-dir 的钩子。
-
post-receive读取标准输入中的 ref 更新信息(如refs/heads/main),适合做分支判断 -
post-update只接收参数(如refs/heads/main),但此时 Git 已经完成对象写入,环境变量不可靠 - 很多教程抄来抄去用
post-update,实际在 Git 2.30+ 上大概率静默失败
post-receive 钩子脚本怎么写才不出错
关键不是“能不能跑”,而是“权限对不对、路径存不存在、分支认不认”。下面是最小可用模板:
#!/bin/sh # 必须 unset GIT_DIR,否则 git 命令会误以为还在裸仓库里 unset GIT_DIR <h1>指定部署目录(必须提前创建,且属主为运行 git 的用户,如 git:git)</h1><p>DEPLOY_PATH="/var/www/myapp" BRANCH="main"</p><h1>检查部署目录是否存在,避免静默失败</h1><p>if [ ! -d "$DEPLOY_PATH" ]; then echo "ERROR: $DEPLOY_PATH does not exist" exit 1 fi</p><h1>切换到部署目录并强制检出指定分支</h1><p>cd "$DEPLOY_PATH" || exit 1 git --git-dir="/repo/myapp.git" --work-tree="$DEPLOY_PATH" checkout -f "$BRANCH" 2>&1</p><h1>修复权限(尤其当 web 服务用户不是 git 时)</h1><p>chown -R www-data:www-data "$DEPLOY_PATH"</p>
-
--git-dir必须指向裸仓库的绝对路径(如/repo/myapp.git),不能用相对路径 -
checkout -f会丢弃部署目录里所有未提交的改动(包括上传文件),这点必须提前迁走 - 如果用 Nginx/Apache,
chown这步不能省——否则 502 或 403 是常态
常见报错和对应解法
部署失败时,post-receive 默认不输出日志,错误全被吞掉。加两行就能定位问题:
- 推送失败提示
remote: fatal: not a git repository→ 检查--git-dir路径是否拼错,或该路径下有没有objects/目录 - 推送成功但页面没更新 → 执行
ls -l /var/www/myapp,看文件时间戳;再手动运行一遍钩子里的git checkout命令,观察报错 -
Permission denied (publickey)本地推送失败 → 不是钩子问题,是 SSH 密钥没配好,检查~git/.ssh/authorized_keys是否包含你的公钥 - 部署后 CSS/JS 404 → 很可能是前端构建产物没提交进 Git,或钩子没触发
npm install && npm run build,这得自己加
真正麻烦的从来不是写几行脚本,而是每次部署都得确认三件事:裸仓库权限、部署目录权限、web 服务用户能否读取那些文件。漏一个,就卡住一小时。











