背景
一台阿里云香港轻量应用服务器(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 clone 或 npm 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.mjs 的 site 字段还是旧域名。这个值会注入到每个页面的 <link rel="canonical">、Open Graph 标签、JSON-LD 和 sitemap 中。
修复:改 astro.config.mjs 的 site 字段,同时检查 public/robots.txt 和 public/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 个坑。大部分坑来自三个根因:
- 权限:macOS → Linux 的权限继承问题
- 网络:国内网络环境下 GitHub/SSH 的可达性
- 配置残留:旧配置在新环境中的隐性影响
如果你也在做类似的事,希望这篇文章能帮你省掉半天时间。