rsync --delete 的几个坑

静态站部署最简单的方式就是 rsyncdist/。但 --delete 这个参数用不好,可以让人印象深刻。

尾斜杠决定了同步的是目录还是内容

这是 rsync 最经典的坑:

rsync -av dist/  user@host:/var/www/site/    # 同步 dist 里的内容
rsync -av dist   user@host:/var/www/site/    # 把 dist 目录本身放进去

差一个斜杠,第二条会在服务器上创建 /var/www/site/dist/,然后你打开网站看到 403,对着 Nginx 配置查半天。

记法:源路径带斜杠 = “这个目录里的东西”,不带 = “这个目录”。目标路径的斜杠没有这个区别。

养成源路径永远带斜杠的习惯,能省掉一整类问题。

—delete 会删掉不属于构建产物的文件

--delete 的作用是让目标端与源端完全一致——源端没有的,目标端就删掉。这正是部署想要的:删掉的页面应该真的消失,而不是变成幽灵路由留在服务器上。

但”完全一致”是字面意思。任何你手动放到服务器上、而不在构建产物里的文件,都会被清掉。

典型的翻车场景:某个第三方平台要你放一个验证文件到网站根目录,你顺手 scp 了上去。下一次部署,文件没了,验证失效——而且你多半不会立刻发现。页脚引用的一张图标也是同理,下次部署后变成裂图。

正确做法是让这类文件进入构建产物。Astro 里就是放进 public/,构建时会原样复制到 dist/ 根目录。

public/verify.txt  →  dist/verify.txt

判断标准很简单:这个文件应该被版本控制吗?应该,那它就该在仓库里,而不是只存在于服务器上。

先演练再执行

--delete 的破坏性操作值得先看一眼。加 -n(等价于 --dry-run):

rsync -avn --delete dist/ user@host:/var/www/site/

输出里以 deleting 开头的行就是即将被删的文件。扫一眼,确认没有意外,再去掉 -n 真跑。

这一步在第一次部署、或者改了构建配置之后特别值得做。日常增量部署可以省。

排除不该同步的东西

.DS_Store 是 macOS 用户的老朋友,会跟着 rsync 一路传到服务器上。

rsync -avz --delete \
  --exclude='.DS_Store' \
  --exclude='.git' \
  dist/ user@host:/var/www/site/

服务器端的 Nginx 也该兜一层,拒绝一切隐藏文件:

location ~ /\. {
    deny all;
}

两边都做。客户端的 exclude 可能被别人的部署脚本绕过,服务器端的规则是最后一道。

-z 不是永远该加

-z 开启传输压缩,在慢速链路上有用。但如果你的产物已经是压缩过的格式——图片、字体、预压缩的 .br 文件——再压一遍只是浪费 CPU,传输量几乎不变。

-avz 是网上抄来最多的组合,但同步一堆 PNG 和 woff2 的时候,-av 可能更快。局域网或者同机房内网基本没必要加 -z

最后落到一条命令

把这些整理进部署脚本,同步之前先做几项自检:

#!/usr/bin/env bash
set -euo pipefail

npm run build

# 构建产物长得不对就别往上推
[ -f dist/index.html ]                  || { echo "✗ 首页缺失"; exit 1; }
grep -q '<title>' dist/index.html       || { echo "✗ 首页没有 title"; exit 1; }
[ "$(find dist -name '*.html' | wc -l)" -gt 1 ] || { echo "✗ 页面数异常"; exit 1; }

rsync -av --delete --exclude='.DS_Store' dist/ user@example.com:/var/www/site/

set -euo pipefail 那行是关键——任何一步失败就停,不会带着半个构建产物同步上去。