hexo的deploy部署命令部署法
本文最后更新于 2026年10月15日 凌晨
之前写过两篇相关的:Hexo的部署(使用git) 讲的是 hexo deploy 用 git 部署的基本原理,服务器自部署Hexo网站实现自动化构建 讲的是在服务器上建裸仓库、用钩子在服务器端构建的方案。这篇算第三个阶段:本地构建,用 hexo deploy 把产物推到服务器,由服务器的钩子负责落地。等于把前两篇的机制各取一半拼起来。
一、为什么要换
服务器端构建那套(push 源码 → 服务器钩子 → 服务器跑 hexo g → rsync 到站点目录)能跑通,但有几个不舒服的地方:
- 服务器上必须装 node / pnpm / hexo,依赖也装在服务器上;
- 每次推送都要在服务器上跑一遍依赖安装和构建,慢;
- 构建报错只能去服务器上看日志,本地复现不了;
- 服务器上多了一套完整的 node 工具链要维护。
而 hexo deploy 的机制其实很简单——它就是把生成的静态文件用 git 推到某个远端。把”推送”和”落地”拆开看,服务端只负责把文件搬到 Web 目录,这件事一个 git 钩子就够,根本不需要 node。
所以就变成了:构建留在本地(环境熟悉、报错看得见),服务器只当一个”收件 + 搬运”的角色。
二、hexo deploy(Git 方式)到底做了什么
它靠 hexo-deployer-git 插件完成,幕后是四步:
- 在项目根目录维护一个隐藏的独立仓库
.deploy_git; - 把
public/里的内容同步进这个仓库; - 在里面
git add -A+git commit -m "Site updated: <时间>"; - 执行
git push -f,推到你配置的远端仓库和分支。
关键认识:它不关心对端是什么。 对端是 GitHub Pages、是 Gitea、还是你自己在服务器上 git init --bare 出来的一个空仓库,对它来说没有区别——它只负责”把生成结果用 git 推过去”。
所以只要对端能接住这个 push 并把文件放到正确的位置,部署就成立了。而”接住 push”这件事,在裸仓库里就是 git 钩子。
三、接收端:裸仓库 + post-update 钩子
为什么是裸仓库
裸仓库没有工作区,它的存在意义就是”接收推送”,正好符合中转站的角色:
1 | |
钩子选哪个
git 提供了 post-receive 和 post-update 两个推送后钩子,区别在参数传递方式:
post-receive:从标准输入读旧值 新值 引用名的三元组;post-update:把引用名作为命令行参数传进来。
单分支、单一目标的部署,post-update 就够了,脚本更简单。
钩子做三件事
- 从裸仓库把本次推送的内容检出到一个暂存目录;
- 用
rsync -a --delete把暂存目录同步到 Web 根目录; - 删掉暂存目录。
为什么不直接 checkout 到 Web 根目录?
因为 git checkout -f 只写不删。本站的永久链接是 /:year/:month/:hash/ 这种带 hash 的形式,只要改一次文章标题,hash 就变了,会多出一个新目录,而旧目录会一直留在站点目录里,变成死链。rsync --delete 才能让目标目录精确等于本次推送的内容。
为什么可以放心用 push -f?
接收端那个仓库只存构建产物,它自己的历史没有任何意义(源码的历史在另一个仓库里)。所以 -f 把之前的提交覆盖掉是正常的——跟 GitHub Pages 的 gh-pages 分支是同一个套路。
四、完整链路
1 | |
最后一环要说明一下:这里的 Nginx 是 root 直接读磁盘上的静态文件,不是反向代理。所以文件一落进 Web 目录,线上立刻就是新的,不需要重载 Nginx,也不需要重启任何服务。
五、实践:服务端
以下命令在 Web 服务器上执行,假设站点目录是 /srv/blog/www,部署账号是 deployer。
1 | |
先确认账号对站点目录有写权限,别等推送失败了再回来查:
1 | |
然后写钩子:
1 | |
三个要点:
<<'EOF'的单引号不能省。省掉的话$STAGE、$WWW会在写入文件时就被当前 shell 展开成空字符串,钩子里就变成rsync -a --delete "/" /了——这是能造成严重事故的。set -e让脚本中途出错立刻停止,避免半成品上线。- 钩子脚本的换行符要用 LF。从 Windows 传过去的 CRLF 会让
/bin/sh报bad interpreter之类的怪错。
Nginx 侧(站点目录就是上面那个 www):
1 | |
注意这里 root 的路径必须和钩子里 WWW 指向的路径完全一致,这是最容易出错、也最难察觉的地方(见下面的坑 3)。
六、实践:本地
第一步,装插件:
1 | |
这个插件不在 Hexo 的默认依赖里,不装的话 hexo d 会直接报:
1 | |
第二步,配置 _config.yml:
1 | |
第三步,推送:
1 | |
rm -rf .deploy_git 这步容易被忽略:hexo-deployer-git 会把远程地址缓存在 .deploy_git/.git 里,不清掉的话即使 _config.yml 改了,它可能还是往旧地址推。
七、怎么确认真的成功了
hexo g -d 的输出里有三个信号:
目标地址正确,且第一次推送时是新建分支而不是强推:
1
2To <服务器IP>:/srv/blog/deploy.git
* [new branch] HEAD -> master钩子的标准输出会以
remote:前缀透传回来——这是最有价值的一条,等于在本地就能看到服务端脚本的执行结果:1
2remote: >>>>>> [Deploy] 开始同步静态文件
remote: >>>>>> [Deploy] 已同步到 /srv/blog/www最后一行:
INFO Deploy done: git
然后到服务器上交叉验证:
1 | |
八、踩过的坑
hexo-deployer-git没装。_config.yml里写了 deploy 段也没用,hexo d直接报Deployer not found: git。部署账号对站点目录没有写权限。钩子里
rsync报Permission denied。先用test -w测,别靠猜。注意账号属主和目录属主是两回事——目录是775且属主是另一个用户时,非属主账号就只有r-x。钩子里的目标目录写错了。这个最坑:推送成功、钩子也执行成功、
Deploy done也打印了,但线上毫无变化——因为文件被搬到了另一个没人读的目录。这种”静默成功”比直接报错难查得多。务必核对 Nginx 的root和钩子里的$WWW是不是同一个路径。只
checkout不delete。改标题后旧永久链接变成死链,而且会一直堆。所以钩子里必须有rsync --delete。换了仓库地址但没清
.deploy_git。它会继续往旧地址推,而你会以为配置没生效。heredoc 没加单引号。变量被提前展开,钩子里的路径全变空字符串。
钩子脚本带了 CRLF。在 Windows 上编辑再传上去很容易发生,
/bin/sh会报莫名其妙的解释器错误。用file <钩子>确认是 “POSIX shell script” 而不是 “with CRLF line terminators”。构建环境自身的版本漂移。比如 pnpm 大版本升级后,原先写在
package.json里pnpm字段的设置(如overrides)不再被读取,pnpm 会拒绝执行任何 script,报ERR_PNPM_VERIFY_DEPS_BEFORE_RUN。这时要把这些设置迁到pnpm-workspace.yaml。这类问题和部署无关,但会在部署前一秒拦住你,所以单独记一笔。配置文件的换行符混用。在 Windows 上编辑
_config.yml,如果编辑器只给新行写 CRLF,就会出现同文件内混用。YAML 一般能吃下去(CR 会被当作行尾),但 git diff 会变得没法读。要根治可以在仓库根放一个.gitattributes:1
* text=auto eol=lf它跟着仓库走,比在每台机器上设
core.autocrlf可靠。
九、和”服务器端构建”方案的对比
| 维度 | 服务器端构建 | 本地构建 + hexo deploy |
|---|---|---|
| 触发方式 | git push 源码,自动 |
手动执行 hexo g -d |
| 服务器依赖 | node + pnpm + hexo | 只需要 git 和 rsync |
| 构建速度 | 每次都要装依赖、生成 | 用本地缓存,通常更快 |
| 出错在哪看 | 服务器日志 | 本地终端 |
| 源码与产物 | 同一个仓库 | 两个仓库,各管各的 |
| 站点目录残留 | 取决于钩子是否用了 --delete |
同上 |
| 适合场景 | 完全不想碰命令行 | 想掌控构建过程、服务器不想装 node |
两者并不冲突,也可以并存:源码照常 push 到自己的代码仓库做版本管理,发布时再单独跑一次 hexo g -d。相当于把”保存”和”发布”两个动作分开了。
十、小结
hexo deploy(Git 方式)的本质就是:把public/内容提交进.deploy_git,然后git push -f到配置的远端。它不关心对端是谁。- 对端的最小实现是:一个裸仓库 + 一个把文件搬到 Web 目录的钩子。
- 钩子的关键细节有两个:检出到暂存目录再用
rsync --delete同步(解决旧文件残留),用push -f(产物仓库的历史无意义)。 - 这样服务器完全不需要 node 环境,构建留在本地,报错看得见,链路短、每一环都能单独验证。
- 代价是发布从”push 即发布”变成了”手动执行一条命令”——对个人博客来说,这个代价换来的是可控性和简单性,是划算的。