一台服务器部署两个站——Caddy + GitHub Actions 自动化踩坑实录

背景

一台阿里云香港轻量应用服务器(Ubuntu 24.04),要跑两个独立的静态站点,各绑一个域名。技术栈是 Astro 构建 + Caddy 托管 + GitHub Actions 自动部署。

听起来不复杂。实际上从凌晨三点半干到晚上八点,踩了一路坑。这篇文章把每个坑和解法都记下来。

架构

GitHub 仓库 A (push) → GitHub Actions → SSH → 服务器 /var/www/site-a/
GitHub 仓库 B (push) → GitHub Actions → SSH → 服务器 /var/www/site-b/

Caddy 根据域名分发:
  domain-a.com → /var/www/site-a/
  domain-b.com → /var/www/site-b/

两个站共用一台服务器、一个 Caddy 实例、一套 Node.js 环境。隔离靠目录和 Caddyfile 的域名块。

坑 1:目录权限 700 → Caddy 403

现象:Caddy 配置正确,文件存在,但返回 403 Forbidden。

原因:rsync 从 macOS 推文件到 Linux 时,目录权限继承了 macOS 的 umask,变成 drwx------(700)。Caddy 以 caddy 用户运行,连目录都进不去。

修复

find /var/www/site -type d -exec chmod 755 {} \;
find /var/www/site -type f -exec chmod 644 {} \;

教训:每次 rsync 部署后必须修权限。文件权限对了不够,目录必须 755。子目录也一样——images/ 如果是 700,里面的图片全部 403。

坑 2:Caddy 端口绑定决定 HTTPS 行为

现象:域名能访问但只有 HTTP,没有 HTTPS 证书。

原因:Caddyfile 里写的是 domain.com:80 {},显式绑定 80 端口后 Caddy 不会自动申请 Let’s Encrypt 证书。

修复:去掉端口绑定,改成 domain.com {},Caddy 自动启用 HTTPS + ACME 挑战。

注意caddy fmt --overwrite 会把 http://domain.com {} 的前缀吞掉,所以用 domain.com:80 {} 代替更可靠。但记住部署前要改回来。

坑 3:全局 :80 块拦截所有流量

现象:某个域名返回 404,其他域名正常。

原因:Caddyfile 里有一个全局的 :80 { redir {uri} https://{host}{uri} } 块,它拦截了所有 80 端口的请求,导致域名特定的 :80 块失效。

修复:删掉全局 :80 块。Caddy 的 domain.com {} 写法会自动处理 HTTP→HTTPS 跳转,不需要手动写。

坑 4:caddy reload 必须指定配置文件

现象caddy reload 报错 Error: no config file to load

原因:不带 --config 参数时 Caddy 不知道读哪个文件。

修复caddy reload --config /etc/caddy/Caddyfile

坑 5:nginx 抢占 80 端口

现象:Caddy 启动失败,日志显示 port 80 already in use。

原因:服务器上残留了旧的 nginx 配置,重启后 nginx 自动启动占用了 80 端口。

修复

fuser -k 80/tcp          # 杀掉占用 80 端口的进程
systemctl disable nginx   # 禁止 nginx 自启动
systemctl restart caddy   # 重启 Caddy

坑 6:阿里云防火墙”已启用”≠ 实际生效

现象:Let’s Encrypt ACME HTTP-01 验证持续超时,防火墙规则显示”已启用”。

原因:阿里云轻量服务器的防火墙规则有时候显示 enabled 但实际不生效。

修复:删除旧规则,重新创建 HTTP(80) 和 HTTPS(443) 规则。不要修改,直接删了重建。

坑 7:Let’s Encrypt 速率限制

现象:ACME 验证失败 5 次后,证书申请被冻结 1 小时。

原因:Let’s Encrypt 对每个域名有失败次数限制(5 次/小时)。

教训:频繁重试只会延长等待时间。第一次失败后先查原因(防火墙?DNS?端口?),确认修复后再试。

坑 8:GitHub Actions 用了内网 IP

现象:GitHub Actions 部署步骤报 dial tcp: connect: no route to host

原因:SERVER_HOST secret 填的是服务器内网 IP(172.17.x.x),GitHub 的 runner 在美国,当然连不上。

修复:改成公网 IP(47.x.x.x)。

坑 9:大仓库在服务器上构建失败

现象:GitHub Actions SSH 到服务器后执行 git clone && npm ci && npm run build,但 cp dist/*No such file or directory

原因:仓库太大(160MB+),服务器上的 git clonenpm ci 超时/失败,但脚本用了 set -e 却没有正确捕获错误,导致继续执行到 cp 步骤。

修复:改为在 GitHub Actions runner 上构建,然后用 scp/rsync 只传 dist/ 到服务器:

- name: Build
  run: npm run build

- name: Prepare server
  uses: appleboy/ssh-action@v1
  with:
    script: rm -rf /var/www/site/*

- name: Upload dist
  uses: appleboy/scp-action@master
  with:
    source: "dist/"
    target: "/var/www/site/"
    strip_components: 1

坑 10:Node.js 版本不够

现象npm run build 报错 Node.js v20.x is not supported by Astro! Please upgrade to >=22.12.0

修复

curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
apt install -y nodejs

坑 11:SSH 从 Mac 连不上服务器

现象ssh root@server:22022 持续超时,但 Workbench(阿里云网页终端)能连。

原因:家里网络的 VPN/代理只代理了浏览器流量,命令行工具的流量走直连,而直连被运营商或 GFW 拦截了 22022 端口。

解决

  • 方案 A:等 SSH 恢复后用 rsync 部署
  • 方案 B:在 Workbench 终端手动执行部署命令
  • 方案 C(最终方案):用 GitHub Actions 自动部署,完全绕过本地 SSH

坑 12:git push 需要手动指定代理

现象:浏览器能打开 GitHub,但 git push 超时。

原因:VPN 只代理浏览器流量(通过浏览器扩展),系统命令行不经过代理。

修复

git -c http.proxy=socks5://127.0.0.1:29290 push origin main

或者永久配置:

git config http.proxy socks5://127.0.0.1:29290

坑 13:构建产物里残留旧域名

现象:部署后所有页面的 canonical URL、og:url、sitemap 都指向错误的域名。

原因astro.config.mjssite 字段还是旧域名。这个值会注入到每个页面的 <link rel="canonical">、Open Graph 标签、JSON-LD 和 sitemap 中。

修复:改 astro.config.mjssite 字段,同时检查 public/robots.txtpublic/og-default.svg 里是否有硬编码的域名。

教训:改域名不是改 Caddyfile 就完了,必须全站扫描构建产物。

最终方案

本地开发 → git commit → git push(SOCKS5 代理)

         GitHub Actions 自动触发

         runner 上 npm ci && npm run build

         scp dist/ → 服务器 /var/www/site/

         SSH 修权限(755/644)

         Caddy 自动服务(已运行中)

全程无需手动 SSH 到服务器。push 即部署,大约 50-70 秒完成。

总结

16 个小时,13 个坑。大部分坑来自三个根因:

  1. 权限:macOS → Linux 的权限继承问题
  2. 网络:国内网络环境下 GitHub/SSH 的可达性
  3. 配置残留:旧配置在新环境中的隐性影响

如果你也在做类似的事,希望这篇文章能帮你省掉半天时间。