返回博客

rsync --exclude 救不了服务器上的 .env.local

用 GitHub Actions 把 Next.js 站点部署到 VPS,每次发布后站点启动失败——日志里清一色是 Missing environment variable。明明 rsync 已经加了 --exclude='.env.local',为什么服务器上的配置文件还是消失了?

这是 rsync 一个极易踩中的语义陷阱,叠加 Next.js output: 'standalone' 的特殊目录布局,以及宝塔面板自带的不可变文件,三坑同时落地。

现象

  • 每次 CI 部署后,服务器 .env.local 消失,应用启动时环境变量为空,功能异常
  • rsync 偶发 Permission denied,整个传输中断
  • 页面 HTML 更新了,但 JS/CSS 资源 404
  • PM2 重载报 Cannot find module './server.js'

根因

坑一:--exclude 是源端过滤,不是目标端保护

这是最隐蔽的一坑。rsync --exclude='.env.local' 的语义是:不把源端的 .env.local 发送过去。但配合 --delete 时,rsync 会把目标端有、源端没有的文件全部删掉。.env.local 在源端本来就不存在(CI 环境不提交敏感文件),所以目标端的 .env.local--delete 清掉了,--exclude 完全没有保护它。

坑:--exclude + --delete 会删掉目标端独有文件

--exclude 告诉 rsync"忽略源端这个路径",但对目标端没有任何保护承诺。只要目标端有、源端没有,--delete 就会删。要保护目标端已有的文件,必须用 --filter='protect <path>'(过滤规则 P,纯目标端操作)。

坑二:Next.js standalone 布局不完整

output: 'standalone' 构建后,可运行产物在 .next/standalone/,但 .next/static/(JS/CSS chunks)和 public/(静态资源)不会被自动复制进去。直接 rsync 这个目录,静态资源 404。

坑三:宝塔 .user.ini 是不可变文件

宝塔面板会对站点目录下的 .user.ini 执行 chattr +i(操作系统级不可变),任何写入都会被内核拒绝,导致 rsync 整体失败。

正解

构建阶段:手动把静态资源拼进 standalone 目录

pnpm build
mkdir -p deploy
cp -r .next/standalone/. deploy/
cp -r .next/static  deploy/.next/static
cp -r public        deploy/public

传输阶段:用 --filter='protect' 替代 --exclude,同时排除宝塔不可变文件

- name: Deploy via rsync
  run: |
    rsync -azP --delete \
      --filter='protect .env.local' \
      --filter='protect data/' \
      --filter='protect .user.ini' \
      --filter='protect .htaccess' \
      --filter='protect proxy_cache_dir/' \
      -e "ssh -i ~/.ssh/deploy_key" \
      deploy/ root@${{ secrets.DEPLOY_HOST }}:/www/wwwroot/app/

启动阶段:PM2 幂等重载 + 健康检查兜底

- name: PM2 reload + health check
  uses: appleboy/ssh-action@v1.2.2
  with:
    script: |
      cd /www/wwwroot/app
      pm2 reload ecosystem.config.js || pm2 start ecosystem.config.js
      sleep 5
      curl -sf http://127.0.0.1:3456/api/health || exit 1
正解:用 protect 过滤规则做目标端保护

--filter='protect <path>' 是目标端操作:rsync 在生成删除列表时会跳过匹配项,无论源端是否存在该文件,目标端都不会被动。用它替代 --exclude 来保护服务器上的运行时配置、持久化目录和宿主机锁定文件。

速查:三条规则
  1. 保护目标端文件 → --filter='protect <path>'
  2. Next.js standalone → 构建后手动 cp .next/staticpublic
  3. 宝塔 .user.ini / .htaccess → 加进 protect 列表,不要尝试覆写

一句话外卖

rsync --exclude 只管"别从我这里发",--delete 不管你加没加 exclude 照样清理目标端——要留住服务器的文件,用 --filter='protect'