Git post-receive 钩子实现 VPS 自动部署
2026/6/21 20:42:30 网站建设 项目流程

1. 项目概述:用 Git 钩子在 VPS 上实现零手动干预的代码发布

你有没有过这样的经历:改完一行 CSS,本地测试 OK,然后打开终端,ssh 连上服务器,cd 到网站目录,git pull,再 reload nginx —— 整套流程 45 秒,但每天重复 12 次,一个月就是 9 小时。更糟的是某天凌晨三点紧急修复线上 bug,手抖输错分支名,回车后页面白屏,而你正困得睁不开眼。这不是效率问题,是运维风险。我做独立开发者和小团队技术顾问这十多年,见过太多人把“自动部署”想得太重:要学 Docker、要配 Jenkins、要搞 CI/CD 流水线……其实对绝大多数静态站点、PHP 博客、Node.js 小工具、Python Flask 后端来说,一套纯 Git 原生机制就能扛起全部发布任务,且全程不依赖任何第三方服务、不装额外软件、不改系统配置,只要你会用 git clone 和 ssh。核心就一句话:把你的 VPS 当成一个“裸仓库”,利用 Git 自带的post-receive钩子,在代码推送到服务器的瞬间,自动执行拉取、构建、重启三步操作。它不神秘,没黑箱,所有命令都是你日常敲过的;它极轻量,整个部署逻辑写在 12 行 shell 脚本里;它极可靠,没有网络超时、服务崩溃、权限错乱这些 CI 工具常见的“玄学故障”。关键词GitVPSautomatic deploymentpost-receivegit hooks全部落在实处——不是概念堆砌,而是你今晚就能照着操作、明早就能上线的完整路径。适合所有用 VPS 托管个人项目、博客、API 服务、内部工具的同学,无论你是刚学会git add .的新手,还是天天写 Ansible Playbook 的老手,这套方案都比你当前用的“手动 scp”或“临时写个 bash 脚本”更干净、更可追溯、更少出错。

2. 整体设计思路与方案选型逻辑

2.1 为什么放弃 Jenkins/GitLab CI/ GitHub Actions?—— 成本、可控性与场景匹配度

很多人一提自动部署,第一反应就是上 CI/CD 平台。我试过 Jenkins,搭好基础环境花了两天,调通 Node.js 构建环境又卡了三天,最后发现 80% 的构建步骤只是npm install && npm run build,而真正耗时的是等 Jenkins Agent 从休眠中唤醒、下载 Docker 镜像、解压依赖包。GitLab CI 更麻烦:私有 GitLab 实例要自己维护 Runner,公有 GitLab 又受限于免费版的分钟数,一次构建超时就得重来。GitHub Actions 对开源项目友好,但如果你的代码在私有 Git 服务器(比如 Gitea 或自建 Gitolite),它根本连不上你的仓库。更重要的是,CI/CD 的本质是“把构建过程从目标服务器剥离出来”,而这对很多 VPS 场景是反直觉的。你的 VPS 就是生产环境,它有 Nginx 配置、SSL 证书、数据库连接、本地缓存目录——这些资源 CI 服务器根本访问不到,你最终还得写一堆scprsync把产物传回去,中间多了一层网络传输、权限校验、路径映射,故障点反而增加了。我帮一个客户迁移时发现,他们用 GitHub Actions 构建 Vue 项目,产物打包完再rsync到 VPS,结果因 VPS 磁盘空间不足导致同步失败,错误日志却藏在 GitHub 的构建日志里,排查花了 40 分钟。而用post-receive,所有操作都在 VPS 本地发生,df -h一眼看穿磁盘,ls -l直接查权限,tail -f .git/hooks/post-receive实时看脚本执行流,没有中间商赚差价。

2.2 为什么选post-receive而非pre-receiveupdate?—— 安全边界与执行时机

Git 钩子有三类:pre-receive(推送前触发)、update(每个 ref 更新前触发)、post-receive(所有 ref 推送完成后触发)。初学者常误以为pre-receive更“安全”,因为能提前拦截非法推送。但实际部署中,pre-receive是最不适合的。原因有二:第一,它运行在“接收数据流”的过程中,此时 Git 仓库的索引和对象库尚未写入完成,你无法安全地git checkoutgit reset --hard,强行操作极易导致仓库损坏;第二,它的返回值决定整个推送是否成功,一旦你的钩子脚本里有个curl超时或npm install失败,用户git push就会报错,但代码其实已经部分上传,状态不一致,后续git push --force都可能救不回来。update钩子虽在每个分支更新前执行,但它只接收refname oldrev newrev三个参数,无法获取推送的完整 commit 列表,也不方便做跨分支的构建策略(比如main分支推送到/var/www/htmlstaging分支推送到/var/www/staging)。post-receive是唯一满足所有条件的:它在所有数据落盘、所有 ref 更新完毕后才执行,此时仓库处于完全一致状态;它接收完整的 stdin 输入(每行一个refname oldrev newrev),可轻松解析推送了哪些分支、哪些 commit;它不阻塞推送流程,即使钩子脚本执行失败,git push依然成功,你只需查服务器日志,不影响开发同学的提交节奏。我在线上跑了三年,post-receive的稳定性远超预期——它甚至能在 VPS 内存只剩 50MB 时正常工作,因为它的启动开销几乎为零。

2.3 为什么坚持“裸仓库 + 工作树分离”?—— 避免 Git 冲突与权限混乱

常见误区是直接在网站根目录(如/var/www/html)初始化一个普通 Git 仓库,然后git push过去。这看似简单,但埋下巨大隐患。Git 普通仓库包含.git目录和工作树(即你看到的文件),当你git push到一个已检出的工作树时,Git 会拒绝,报错refusing to update checked out branch。有人会加receive.denyCurrentBranch updateInstead配置绕过,但这会导致工作树文件与 Git 索引不同步:比如你git push一个新文件,.git/index记录了它,但工作树文件可能因权限问题没写入,下次git status就显示modified,而你根本不知道它被谁改了。更危险的是,如果网站程序(如 PHP)正在读取某个文件,而 Git 正在checkout它,可能出现“文件被占用”或“读到半截内容”。正确解法是裸仓库(bare repository)+ 工作树(working tree)分离。裸仓库只有.git目录,没有工作树,纯粹用于接收推送,绝对安全;工作树单独放在另一个目录(如/var/www/html),由post-receive钩子在推送后git --work-tree=/var/www/html --git-dir=/var/repo/site.git checkout -f命令强制覆盖。这样,接收和部署完全解耦:裸仓库永远只做“收件箱”,工作树永远只做“展示厅”,两者之间通过原子性checkout操作桥接,不存在中间态。我曾帮一个电商客户修复过类似问题:他们用普通仓库部署,某次git push后网站图片加载失败,查了半天发现是.git/index里记录的图片大小是 120KB,但工作树里的文件只有 80KB(写入被中断),而 Nginx 缓存了这个残缺文件,清缓存都不管用,最后只能手动cp完整文件过去。用裸仓库后,这种问题彻底消失。

2.4 为什么不用git pull而用git checkout?—— 原子性与环境纯净度

另一个高频误区是:在 VPS 上建一个普通仓库,然后post-receivecd /var/www/html && git pull origin main。这看起来很自然,但存在两个硬伤。第一,git pull本质是git fetch + git merge,而merge操作会生成新的 merge commit,污染你的提交历史。你本地git log显示的是清晰的线性提交,但服务器上却多出一堆Merge branch 'main' of ...,不仅难看,还可能导致后续git revert出错。第二,pull依赖远程跟踪分支(如origin/main),而裸仓库默认不创建这些分支,需要额外git remote add origin ...配置,增加复杂度。git checkout -f则完全不同:它直接从裸仓库的HEAD指向的 commit,强制覆盖工作树所有文件,不产生任何新 commit,不修改任何分支指针,是纯粹的“快照还原”。它保证每次部署都是 100% 确定的状态——你git push的是什么 commit,服务器上就是什么 commit,不多不少,不增不减。我在部署一个实时聊天 API 时,必须确保所有服务器节点代码完全一致,否则 WebSocket 协议版本错乱会导致客户端断连。用checkout -f后,我写了个简单校验脚本:ssh user@vps "cd /var/www/api && git rev-parse HEAD",对比本地git rev-parse HEAD,毫秒级确认一致性。换成pull,这个校验就失效了,因为你永远不知道它到底merge了什么。

3. 核心细节解析与实操要点

3.1 VPS 环境准备:最小化依赖与权限隔离

自动部署的基石是干净、可控的 VPS 环境。我坚持“最小化安装”原则:不装任何非必要软件,不改默认 SSH 配置,不碰系统级 Git 设置。第一步,确认 Git 已安装且版本 ≥ 2.15(git --version),这是--work-tree--git-dir参数稳定支持的最低版本。如果版本太低(如 CentOS 7 默认的 1.8.x),不要急着yum update git(可能破坏系统依赖),而是用源码编译安装到/usr/local/bin/git,然后export PATH="/usr/local/bin:$PATH"加入用户 profile。第二步,创建专用部署用户,绝不使用 root 或已有业务用户。执行sudo adduser deploy --disabled-password --gecos "",然后sudo usermod -aG www-data deploy(Ubuntu/Debian)或sudo usermod -aG nginx deploy(CentOS/RHEL),让deploy用户能写入 Web 服务器目录。第三步,生成 SSH 密钥对(ssh-keygen -t ed25519 -C "deploy@your-vps"),将公钥id_ed25519.pub内容追加到~deploy/.ssh/authorized_keys,并设置严格权限:chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys。关键点在于authorized_keys的权限——如果设成 644,SSH 会忽略该文件,导致密钥登录失败,而错误提示极其隐蔽(Permission denied (publickey)),新手常在此卡住数小时。第四步,禁用密码登录:编辑/etc/ssh/sshd_config,确认PasswordAuthentication noPubkeyAuthentication yes,然后sudo systemctl restart sshd。这一步看似与部署无关,实则至关重要:它确保所有对 VPS 的代码推送都经过密钥认证,杜绝暴力破解,也避免了在post-receive脚本里硬编码密码的高危操作。

3.2 裸仓库创建与钩子脚本编写:12 行搞定核心逻辑

裸仓库的创建必须精准。登录deploy用户,执行mkdir -p /var/repo && cd /var/repo && git init --bare site.git。注意路径:/var/repo是约定俗成的裸仓库根目录,site.git是仓库名(可自定义,但必须以.git结尾,这是 Git 识别裸仓库的标志)。此时ls /var/repo/site.git应显示branches/ config description HEAD hooks/ info/ objects/ refs/绝不能有indexHEAD(非符号链接)或任何源码文件。接下来是灵魂——post-receive钩子脚本。进入/var/repo/site.git/hooks/,用nano post-receive创建文件,写入以下内容:

#!/bin/bash # 3.1 获取推送的分支名(假设只部署 main 分支) while read oldrev newrev refname; do branch=$(git rev-parse --symbolic --abbrev-ref $refname) if [ "$branch" = "main" ]; then # 3.2 定义工作树路径(网站根目录) WORK_TREE="/var/www/html" GIT_DIR="/var/repo/site.git" # 3.3 强制检出,覆盖工作树 git --work-tree="$WORK_TREE" --git-dir="$GIT_DIR" checkout -f main # 3.4 执行构建命令(如 npm build、composer install) if [ -f "$WORK_TREE/package.json" ]; then cd "$WORK_TREE" && npm ci --only=production && npm run build elif [ -f "$WORK_TREE/composer.json" ]; then cd "$WORK_TREE" && composer install --no-dev --optimize-autoloader fi # 3.5 重启 Web 服务(根据实际服务名调整) sudo systemctl reload nginx echo "Deployed branch '$branch' to $WORK_TREE" fi done

保存后,chmod +x post-receive赋予执行权限。这段脚本的精妙之处在于:第一,它用while read循环处理 stdin,能同时响应多个分支推送(如maindevelop);第二,git rev-parse --symbolic --abbrev-ref是解析分支名的最可靠方法,比echo $refname | cut -d'/' -f3更健壮;第三,构建命令用if-elif判断项目类型,避免在非 Node.js 项目里执行npm报错;第四,npm ci --only=productionnpm install更快更干净,它跳过devDependencies,且校验package-lock.json一致性。我曾在一个 Laravel 项目里漏写了--no-dev,结果vendor/目录里多了phpunit等测试工具,被扫描器误判为漏洞入口,安全审计时被打了低分。

3.3 本地仓库配置:一键推送与分支映射

本地配置决定了推送的便捷性。进入你的本地项目根目录,执行git remote add production deploy@your-vps-ip:/var/repo/site.git。这里production是远程别名(可自定义),deploy@your-vps-ip是 VPS 的部署用户和 IP,/var/repo/site.git是裸仓库路径。验证是否生效:git remote -v应显示production deploy@your-vps-ip:/var/repo/site.git (fetch)(push)。关键一步是设置推送默认分支映射。执行git config remote.production.push "refs/heads/main:refs/heads/main",这告诉 Git:每次git push production时,自动把本地main分支推送到远程main分支。如果不设,首次推送需git push production main,后续才能省略分支名。更进一步,可以设git config remote.production.mirror true,启用镜像模式,但一般不需要。测试推送:git push production main。如果一切顺利,你应该看到类似Writing objects: 100% (3/3), 256 bytes | 256.00 KiB/s, done.的输出,然后 VPS 上post-receive脚本自动执行,/var/www/html目录被更新。如果报错fatal: '/var/repo/site.git' does not appear to be a git repository,检查两点:一是路径是否拼写错误(site.git不是site),二是deploy用户对该路径是否有读写权限(ls -ld /var/repo应显示drwxr-xr-x 3 deploy deploy)。

3.4 权限与 SELinux 细节:那些让你抓狂的“Permission denied”

权限问题是最常见的拦路虎,尤其在 CentOS/RHEL 系统上。即使deploy用户属于nginx组,/var/www/html目录仍可能因 SELinux 策略被阻止写入。先确认 SELinux 状态:sestatus。如果current modeenforcing,执行sudo setsebool -P httpd_can_network_connect 1(允许 Web 服务联网,如 Composer 下载包),再执行sudo semanage fcontext -a -t httpd_sys_rw_content_t "/var/www/html(/.*)?",然后sudo restorecon -Rv /var/www/html。这三步是标准操作,缺一不可。对于文件权限,我采用“组继承”策略:sudo chgrp -R www-data /var/www/html(Ubuntu)或sudo chgrp -R nginx /var/www/html(CentOS),然后sudo chmod -R g+rws /var/www/htmlg+s(setgid)是关键——它确保新创建的文件和目录自动继承父目录的组,避免git checkout后文件组变成deploy,Nginx 无法读取。另外,post-receive脚本里sudo systemctl reload nginx会失败,因为deploy用户默认无sudo权限。解决方法是编辑/etc/sudoerssudo visudo,添加deploy ALL=(ALL) NOPASSWD: /bin/systemctl reload nginx。注意必须用visudo,它会语法检查,直接编辑/etc/sudoers文件损坏会导致所有sudo失效,系统瘫痪。我曾因手误多打一个空格,sudo报错syntax error,连sudo su都进不去,最后靠 VPS 控制台重置密码才救回。

4. 实操过程与核心环节实现

4.1 从零开始的完整部署流程:手把手带你走一遍

现在,我们把前面所有知识点串起来,模拟一次真实部署。假设你有一个简单的 HTML 博客,本地路径是~/my-blog,VPS IP 是192.168.1.100,部署用户是deploy

第一步:VPS 初始化

# 登录 VPS(用 root 或有 sudo 权限的用户) ssh root@192.168.1.100 # 创建 deploy 用户 sudo adduser deploy --disabled-password --gecos "" # 添加到 www-data 组(Ubuntu) sudo usermod -aG www-data deploy # 切换到 deploy 用户 sudo su - deploy # 创建裸仓库目录 mkdir -p /var/repo cd /var/repo git init --bare my-blog.git # 创建网站目录 sudo mkdir -p /var/www/html sudo chown -R deploy:www-data /var/www/html sudo chmod -R g+rws /var/www/html # 退出 deploy,回到 root exit # 配置 sudo 权限(允许 deploy 重启 nginx) echo "deploy ALL=(ALL) NOPASSWD: /bin/systemctl reload nginx" | sudo tee /etc/sudoers.d/deploy # 如果是 CentOS,替换 www-data 为 nginx,并执行: # sudo setsebool -P httpd_can_network_connect 1 # sudo semanage fcontext -a -t httpd_sys_rw_content_t "/var/www/html(/.*)?" # sudo restorecon -Rv /var/www/html

第二步:编写并激活钩子脚本

# 切换到 deploy 用户 sudo su - deploy # 编辑钩子 nano /var/repo/my-blog.git/hooks/post-receive # 粘贴前面的 12 行脚本,修改 WORK_TREE 为 "/var/www/html",GIT_DIR 为 "/var/repo/my-blog.git" # 保存后赋权 chmod +x /var/repo/my-blog.git/hooks/post-receive

第三步:本地配置与首次推送

# 在本地终端,进入博客目录 cd ~/my-blog # 初始化本地 Git(如果还没做) git init git add . git commit -m "Initial commit" # 添加远程仓库 git remote add production deploy@192.168.1.100:/var/repo/my-blog.git # 推送 main 分支 git push production main

第四步:验证与调试推送后,立即登录 VPS 查看效果:

# 查看钩子执行日志(如果有 echo 输出) # 检查网站目录是否更新 ls -la /var/www/html # 查看最新 commit cd /var/www/html && git rev-parse HEAD # 检查 Nginx 是否正常 curl -I http://192.168.1.100 # 如果 502 错误,检查 Nginx 错误日志 sudo tail -f /var/log/nginx/error.log

首次推送成功后,/var/www/html里应该有你本地的所有文件,且git status显示On branch main, nothing to commit, working tree clean。这意味着工作树与裸仓库HEAD完全一致,部署完成。

4.2 多环境部署:main/staging/develop 分支的差异化处理

单一分支部署满足不了真实需求。比如main推送到生产/var/www/htmlstaging推送到预发/var/www/stagingdevelop推送到开发/var/www/develop。只需扩展post-receive脚本的if-elif逻辑:

while read oldrev newrev refname; do branch=$(git rev-parse --symbolic --abbrev-ref $refname) case "$branch" in "main") WORK_TREE="/var/www/html" SERVICE="nginx" ;; "staging") WORK_TREE="/var/www/staging" SERVICE="nginx" ;; "develop") WORK_TREE="/var/www/develop" SERVICE="nginx" ;; *) echo "Ignoring branch $branch" continue ;; esac git --work-tree="$WORK_TREE" --git-dir="/var/repo/my-blog.git" checkout -f "$branch" # 构建命令同上... sudo systemctl reload "$SERVICE" echo "Deployed $branch to $WORK_TREE" done

本地推送时,指定分支即可:git push production staging。注意,stagingdevelop分支在裸仓库里是“虚拟”的——它们只存在于refs/heads/下,不会自动创建,首次推送需git push production refs/heads/staging:refs/heads/staging。为简化,可在本地git config中为每个环境设置不同远程:

git remote add staging deploy@192.168.1.100:/var/repo/my-blog.git git remote add develop deploy@192.168.1.100:/var/repo/my-blog.git

然后git push staging staginggit push develop develop。这样,不同环境完全隔离,互不影响,main的 bug 不会污染staging的测试。

4.3 构建阶段深度定制:PHP、Node.js、Python 的实战适配

post-receive的强大在于它是一段可编程的 shell 脚本,能无缝集成各种构建工具。以下是三个主流场景的实操模板:

PHP(Laravel):

# 在钩子脚本的构建段加入 if [ -f "$WORK_TREE/.env" ]; then cd "$WORK_TREE" # 安装依赖(跳过 dev) composer install --no-dev --optimize-autoloader # 生成优化文件 php artisan config:clear php artisan cache:clear php artisan view:clear # 生成 autoload composer dump-autoload --optimize fi

关键点:--no-dev防止phpunit等被装到生产环境;config:clear确保.env修改立即生效;dump-autoload --optimize提升类加载速度。我曾因漏掉config:clear,导致.env里改了数据库密码,但 Laravel 还在用缓存的旧配置,连不上 DB,查了两小时。

Node.js(Express):

if [ -f "$WORK_TREE/package.json" ]; then cd "$WORK_TREE" # 清理 node_modules(可选,确保干净) rm -rf node_modules # 使用 ci 安装(比 install 更快更准) npm ci --only=production # 构建前端(如果有的话) if [ -d "$WORK_TREE/client" ]; then cd client && npm ci && npm run build && cd .. fi # 重启 PM2 进程(假设用 PM2 管理) pm2 reload app.js --update-env fi

npm ci是核心,它不读package.json,只按package-lock.json安装,100% 复现本地环境;pm2 reload是热重载,不中断服务,比pm2 restart更平滑。

Python(Flask):

if [ -f "$WORK_TREE/requirements.txt" ]; then cd "$WORK_TREE" # 创建虚拟环境(避免污染系统 Python) python3 -m venv venv source venv/bin/activate pip install --upgrade pip pip install -r requirements.txt # 收集静态文件(Django 类似) if [ -f "$WORK_TREE/manage.py" ]; then python manage.py collectstatic --noinput fi # 重启 Gunicorn(假设用 systemd 管理) sudo systemctl restart my-flask-app fi

虚拟环境venv是必须的,它隔离依赖,避免pip install影响系统全局包;collectstatic是 Django 项目的关键步骤,把所有 CSS/JS 打包到STATIC_ROOT,供 Nginx 直接服务。

4.4 安全加固与审计追踪:让部署可监控、可回滚

自动部署不是“黑盒”,必须有审计能力。第一,记录每次部署日志。在post-receive开头添加:

LOG_FILE="/var/log/deploy.log" echo "[$(date)] Push received: $oldrev -> $newrev on $refname by $(whoami)" >> "$LOG_FILE"

第二,保留历史版本。在钩子脚本末尾加入:

# 创建时间戳备份(保留最近 5 个) BACKUP_DIR="/var/backups/my-blog" mkdir -p "$BACKUP_DIR" tar -czf "$BACKUP_DIR/backup-$(date +%Y%m%d-%H%M%S).tar.gz" -C /var/www html # 清理旧备份 ls -t "$BACKUP_DIR"/backup-*.tar.gz | tail -n +6 | xargs -r rm

这样,任何时候都能tar -xzf backup-20231001-120000.tar.gz回滚到任意时刻。第三,限制推送来源。虽然 SSH 密钥已认证,但可进一步在post-receive里加 IP 白名单:

CLIENT_IP=$(echo $SSH_CONNECTION | awk '{print $1}') ALLOWED_IPS="192.168.1.50 203.0.113.25" if ! echo "$ALLOWED_IPS" | grep -q "$CLIENT_IP"; then echo "Rejected: IP $CLIENT_IP not allowed" exit 1 fi

第四,防止重复部署。有时网络抖动导致git push重试,钩子被多次触发。加个锁文件:

LOCK_FILE="/tmp/deploy.lock" if [ -f "$LOCK_FILE" ]; then echo "Deploy already running" exit 0 fi touch "$LOCK_FILE" # ... 部署逻辑 ... rm -f "$LOCK_FILE"

这些不是“锦上添花”,而是生产环境的底线。我管理的一个金融类后台,就因没做备份,一次误操作git push --force覆盖了main,所有客户配置丢失,靠手动从日志里恢复,花了 8 小时。

5. 常见问题与排查技巧实录

5.1 “fatal: not a git repository (or any of the parent directories): .git” —— 路径与上下文陷阱

这是新手遇到的第一道墙,90% 的原因是post-receive脚本里git命令的执行路径错了。post-receive钩子在/var/repo/my-blog.git/hooks/目录下运行,但git checkout需要知道--git-dir(裸仓库位置)和--work-tree(工作树位置)。如果写成git checkout -f,Git 会尝试在当前目录(即hooks/)找.git,当然找不到。必须显式指定--git-dir--work-tree。另一个常见原因是WORK_TREE路径不存在或权限不足。比如你设WORK_TREE="/var/www/html",但/var/www/html目录还没mkdir,或者deploy用户没写入权限。排查步骤:登录 VPS,手动执行钩子里的命令:

sudo su - deploy cd /var/repo/my-blog.git/hooks/ # 模拟推送输入 echo "0000000000000000000000000000000000000000 abc1234567890123456789012345678901234567 refs/heads/main" | ./post-receive

观察错误输出,它会精确告诉你哪一行git命令失败。如果是fatal: Not a git repository,立刻检查--git-dir路径是否拼写正确,ls /var/repo/my-blog.git是否存在。

5.2 “Permission denied (publickey)” —— SSH 密钥的 7 个致命细节

密钥登录失败是第二大痛点,原因极其琐碎。我整理了 7 个必查点:

  1. 公钥格式id_rsa.pubid_ed25519.pub的内容必须是单行,以ssh-rsa AAAA...ssh-ed25519 AAAA...开头,结尾是邮箱,中间不能换行。复制时别把换行符带进去。
  2. authorized_keys 权限~deploy/.ssh/authorized_keys必须是600-rw-------),~deploy/.ssh必须是700drwx------)。644755都会被 SSH 忽略。
  3. 用户主目录权限~deploy目录不能是777,必须是755700。SSH 要求主目录不能被组或其他人写入。
  4. SELinux 上下文:在 CentOS/RHEL 上,~deploy/.ssh目录的 SELinux type 必须是ssh_home_t。用ls -Z ~deploy/.ssh查看,如果不是,执行sudo semanage fcontext -a -t ssh_home_t "/home/deploy/.ssh(/.*)?" && sudo restorecon -Rv /home/deploy/.ssh
  5. sshd_config 配置:确认/etc/ssh/sshd_configPubkeyAuthentication yesAuthorizedKeysFile .ssh/authorized_keysPasswordAuthentication no(如果禁用密码)都正确,且没有被Include的其他文件覆盖。
  6. 密钥类型兼容性:较老的 OpenSSH 版本(< 7.0)不支持ed25519,如果 VPS 是旧系统,改用ssh-keygen -t rsa -b 4096生成 RSA 密钥。
  7. 代理转发干扰:如果你本地用了ssh -A代理转发,可能影响密钥选择。临时去掉-A参数测试。

5.3 钩子脚本不执行?—— Git 钩子的静默失败机制

post-receive最坑的一点是:它静默失败。即使脚本里echo "hello",你也看不到输出;即使git checkout报错,git push依然显示success。这是因为钩子的 stdout/stderr 被 Git 丢弃了。解决方案是强制重定向日志:

# 在 post-receive 第一行加 exec > /var/log/post-receive.log 2>&1 echo "[$(date)] Script started" # ... 其余脚本 ... echo "[$(date)] Script finished"

然后tail -f /var/log/post-receive.log实时查看。另一个常见原因是脚本开头没写#!/bin/bash,或者写了#!/usr/bin/env bash但 VPS 没装bash(极少见,但 Alpine Linux 默认只有ash)。用file /var/repo/my-blog.git/hooks/post-receive查看脚本类型,确保是shell script。如果脚本里用了source命令,记得sourcebash

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询