社交媒体

Git提交代码自动同步宝塔

Git提交代码自动同步宝塔

改一行代码上线要几步?

打包、登录宝塔、找到站点目录、上传覆盖、清缓存——周末改个错别字也得走一遍。更惨的是某次改完急着走,本地推了,服务器没拉,线上跑着三天前的版本,用户反馈的 bug 你在这边怎么都复现不出来。

这篇把这个环节彻底砍掉:本地 git push 之后,服务器自己把代码拉下来,全程你不用再登面板。

番外-Git同步链路图.png

01 先搞清它在干什么

原理一句话:代码仓库被 push 时,会往你留的一个网址发一条通知(HTTP 请求),宝塔收到通知就去执行一句脚本,脚本做的事就是 git pull。

三个角色:

角色 干什么
Gitee / GitHub / GitLab 事件源:你 push 了,它就发通知
宝塔 WebHook 插件 接收器:守着一个网址,收到请求就跑脚本
同步脚本(shell) 执行者:git pull + 修权限 + 写日志

所以整套东西没有任何黑科技,难的全在细节:脚本放哪、谁来跑、权限怎么给、失败了去哪看日志。

顺带说个安全提醒:宝塔给的 WebHook 地址里带一串密钥(access_key),谁拿到谁就能让你的服务器拉代码。别把它贴到公开仓库、群聊、截图里;服务器上有防火墙的,建议只放行码云/GitHub 的回调 IP。

02 服务器上先把 Git 和钥匙准备好

进宝塔 → 终端,先看有没有 git(宝塔机器一般自带):

git --version

image-olkH.png

没有就装:

# CentOS / 宝塔常见
yum install -y git
# Ubuntu / Debian
apt-get install -y git

生成 SSH 钥匙(一路回车三下,不要设密码,否则脚本拉代码会卡住):

ssh-keygen -t rsa -C "bt-deploy"
cat ~/.ssh/id_rsa.pub

把 cat 出来的那一整串复制下来(以 ssh-rsa 开头、以你的主机名结尾),粘到 Gitee → 设置 → SSH 公钥,标题会自动生成。

2.png

验证服务器和码云是否握手成功(这一步不能跳过):

ssh -T git@gitee.com
# 看到 Hi XXX! You've successfully authenticated 就成了

这一步的意义:让服务器有了"免密拉代码"的身份。后面脚本用 SSH 地址拉代码,就不会在深夜因为要输密码而静默失败。

03 站点目录:先把仓库 clone 下来

进 /www/wwwroot(宝塔站点默认都在这),用 SSH 地址 clone:

cd /www/wwwroot
git clone git@gitee.com:你的用户名/你的仓库名.git my-site

⚠️ 目录名记住它——它就是后面 WebHook 地址里要传的参数。仓库名和站点目录名保持一致,整套配置最省事。

clone 完务必把归属改回 www(宝塔站点的运行用户,不改的话 Web 访问可能 403):

chown -R www:www /www/wwwroot/my-site

再放行一次 git 的目录安全检查(避免后面脚本报错 detected dubious ownership):

git config --global --add safe.directory /www/wwwroot/my-site

04 装宝塔 WebHook,填脚本

宝塔面板 → 软件商店 → 搜 WebHook → 安装。装完打开它,添加 Hook,名称随便写(比如 sync-site),执行脚本贴下面这份:

#!/bin/bash
##############################################################################
# 橙码酱 · 宝塔 WebHook 自动同步脚本
#
# v2 相比网上流传的版本修了三件事,都是真踩过的坑:
#   1. 同一秒被触发多次 → 加并发锁,只跑一个,其余打印 [SKIP] 直接退出
#   2. SSH 认证失败时 git 会卡在"等你输密码",脚本永远不返回 → BatchMode,失败立刻报错
#   3. git 的报错走 stderr,不进宝塔日志 → exec 2>&1,报错一并可见
##############################################################################

exec 2>&1                        # 让 git 的报错也进宝塔日志,否则你只能看到一半

# ====================== 只需改这三行 ======================
GIT_USER="你的码云用户名"        # gitee.com 后面的用户名
BRANCH="master"                  # 要同步的分支(main / master / dev …)
WWW_ROOT="/www/wwwroot"          # 站点根目录
# =========================================================

LOG_DIR="/www/wwwlogs/deploy"    # 部署日志目录,排错全靠它

# 防卡死:SSH 不弹密码框、连不上就 10 秒超时报错;https 地址也不弹凭据框
export GIT_SSH_COMMAND="ssh -o BatchMode=yes -o ConnectTimeout=10"
export GIT_TERMINAL_PROMPT=0

echo ""
echo "===== $(date "+%Y-%m-%d %H:%M:%S") Start ====="

# ---------- 0. 取参数:param 传的就是站点目录名 ----------
if [ -z "$1" ]; then
    echo "[ERROR] param 为空:WebHook 地址请带 &param=站点目录名"
    echo "===== End ====="
    exit 1
fi

SITE="$1"
gitPath="$WWW_ROOT/$SITE"
gitHttp="git@gitee.com:${GIT_USER}/${SITE}.git"   # SSH 地址;注释用 #,不能用 //
echo "[STEP1] 参数 OK:站点目录 = $gitPath"

# ---------- 1. 并发锁:同一秒被触发多次时只跑一个 ----------
LOCK="/tmp/bt_sync_${SITE}.lock"
if ! mkdir "$LOCK" 2>/dev/null; then
    echo "[SKIP] 上一次同步还没跑完,本次跳过(避免 git 抢锁卡死)"
    echo "===== End ====="
    exit 0
fi
trap 'rm -rf "$LOCK"' EXIT

echo "[STEP2] 拿到锁,开始同步"

# ---------- 2. 目录不存在就创建并克隆 ----------
if [ ! -d "$gitPath" ]; then
    echo "[STEP3] 目录不存在,创建并克隆"
    mkdir -p "$gitPath"
    cd "$gitPath" || exit 1
    git clone -b "$BRANCH" "$gitHttp" tmp_clone
    mv tmp_clone/.git ./
    rm -rf tmp_clone
fi

cd "$gitPath" || exit 1

# ---------- 3. 放行 git 安全目录检查(避免 dubious ownership 报错)----------
git config --global --add safe.directory "$gitPath"
echo "[STEP4] 进入站点目录,开始拉取"

# ---------- 4. 强制对齐远端 ----------
# 部署目录不该有人手改,所以用 reset --hard 而不是 pull:
# pull 在本地有改动/换行符差异时会因冲突失败,reset 则是"和仓库一模一样"
git fetch --all
git reset --hard "origin/$BRANCH"
echo "[STEP5] 代码已对齐"

# ---------- 5. 归属还给 www(宝塔站点运行用户)----------
chown -R www:www "$gitPath"

# ---------- 6. 记一笔日志,方便事后查 ----------
mkdir -p "$LOG_DIR"
{
    echo "----- $(date "+%Y-%m-%d %H:%M:%S") -----"
    echo "站点目录:$gitPath"
    echo "当前版本:$(git rev-parse --short HEAD)"
    git log -1 --pretty=format:"%h %an %ad %s" --date=format:"%Y-%m-%d %H:%M"
    echo ""
} >> "$LOG_DIR/${SITE}.log" 2>&1

echo "[STEP6] 已更新到:$(git rev-parse --short HEAD)"
echo "===== End ====="
exit 0

番外-Git同步三坑.png

保存之后,再点一次「编辑」,确认脚本原样还在。宝塔有时候会过滤掉部分字符,脚本残缺时它照样返回 200,但服务器什么都没做——这个坑能让你排查一晚上。

05 拿到地址,回填到仓库

4.png

保存后,WebHook 界面会给你一条地址,长这样:

http://你的服务器IP:8888/hook?access_key=一串密钥&param=aaa

把最后的 aaa 改成你的站点目录名(也就是 03 步 clone 出来的那个名字),然后整条复制。

打开 Gitee 仓库 → 管理 → WebHooks → 添加,把地址粘进去,事件勾 Push(有需要再勾 Tag Push),保存。

改完面板记得重启一次宝塔,让权限和环境变量彻底生效:

/etc/init.d/bt restart

GitHub / GitLab 同款操作:都是把这条 URL 填到仓库的 Webhooks → Payload URL 里,Content type 选默认的即可——宝塔那边不解析请求体,只看 URL 里的 param。

06 验证:别急着改代码

最快的验证不需要 push——直接浏览器访问那条 WebHook 地址(或用 curl):

curl "http://你的服务器IP:8888/hook?access_key=一串密钥&param=my-site"

回来看三样东西:

看哪 期望结果
Gitee → WebHooks → 查看更多 状态码 200
宝塔 WebHook → 日志 出现你写的时间戳、Reset 结果、最新 commit 号
站点目录文件 修改时间和刚才一致

三样都对上了,再在本地改一行、commit、push,亲眼看着服务器目录自己变。那一刻的爽感,值这一篇的收藏。

07 日志只有 Start,永远等不到 End

这是我自己在服务器上真遇到的一幕——日志长这样,整整九行 Start,一行 End 都没有:

===== 2026-10-04 21:45:13 Start =====
===== 2026-10-04 21:45:13 Start =====
===== 2026-10-04 21:45:13 Start =====
===== 2026-10-04 21:45:13 Start =====
===== 2026-10-04 21:45:13 Start =====
===== 2026-10-04 21:45:19 Start =====
===== 2026-10-04 22:02:12 Start =====
===== 2026-10-04 22:03:15 Start =====
===== 2026-10-04 22:31:15 Start =====

先读这段日志:同一个「21:45:13」触发了五次。这本身就是最大的线索。

Start 是脚本第一行打印的,它出现了,说明「宝塔收到请求 → 拉起脚本 → 脚本开始跑」这条路是通的,问题不在 WebHook 配置,而在脚本中途卡住/静默死掉。卡住的位置按概率排:

排名 卡在哪 为什么会卡
1 git fetch(等你输密码) 用了 HTTPS 地址,或 SSH key 没配对。git 想问你要账号密码,可 WebHook 没有终端,它就一直等——等到天荒地老
2 git fetch(抢 .git/index.lock) 五条同一秒触发,五个进程同时摸同一个仓库,git 互相等锁
3 报错静默吞掉 git 的报错走 stderr,宝塔日志只接 stdout,你看到的就是「啥也没有」

三条里前两条有一个共同点:脚本一直在往 End 走,但永远走不到。

所以加固版做了三件事(就是上面脚本里那三行看着"多余"的配置):

  • export GIT_TERMINAL_PROMPT=0 + GIT_SSH_COMMAND="ssh -o BatchMode=yes" → 认证失败立刻报错返回,绝不弹框等待
  • mkdir "$LOCK" 原子并发锁 → 同一秒触发 N 次,只跑一个,其余打印 [SKIP] 直接 End 退出
  • exec 2>&1 → git 的报错并进日志,再也不会「一片空白」

外加 [STEP1]~[STEP6] 六步面包屑:日志停在 STEP 几,就是卡在第几步,不用再猜。

真排查时,别先改业务代码,先跑一遍探针脚本(bt-webhook-probe.sh,打印 [P0]~[P6] 的环境信息,跑到 Probe End 说明执行链路没问题)。另外记得清掉已经卡住的僵尸进程:

ps -ef | grep -E "git|hook" | grep -v grep
kill -9 上面查出来的进程号
rm -f /www/wwwroot/你的站点目录/.git/index.lock

排错清单:报错基本就这 6 种

现象 原因 一句解决
WebHook 日志只有 [ERROR] param 为空 地址里没带 param,或拼错了参数名 在 URL 末尾补 ¶m=站点目录名
Permission denied (publickey) SSH 公钥没配对,或用了 HTTPS 地址 回到 02 步 ssh -T git@gitee.com 验一次
detected dubious ownership 仓库属主和执行者不是一个人 git config --global --add safe.directory 目录
sudo: command not found / sudo 需要 tty 网上教程习惯性加 sudo,面板里本来就是 root 把 sudo 全删掉
返回 200 但代码没变 脚本被面板过滤,或者改的不是你拉的分支 重新编辑脚本确认;检查 BRANCH 变量
只有 Start 没有 End git 在等你输密码、或多个进程抢 .git/index.lock 换加固版脚本(BatchMode + 并发锁),再清 index.lock

速查表

环节 关键命令 / 位置
生成钥匙 ssh-keygen -t rsa → cat ~/.ssh/id_rsa.pub
配公钥 Gitee → 设置 → SSH 公钥
握手验证 ssh -T git@gitee.com
首次 clone /www/wwwroot 下 git clone git@…
权限 chown -R www:www 目录
放行安全目录 git config --global --add safe.directory 目录
WebHook 地址 http://IP:8888/hook?access_key=…¶m=站点目录名
触发方式 Gitee/GitHub 的 Push 事件,或手动 curl 那条地址
看日志 宝塔 → WebHook → 日志

一句话总结:公钥打通身份,WebHook 打通通知,脚本负责最后那一下 reset --hard。 三样都通了,push 就是上线。


橙码酱,分享更有用的技术知识,助你成为更优秀的开发者。