项目背景

个人技术站点不需要为了“现代化”长期运行 Node.js、数据库和容器编排。对于 2 vCPU、1 GB 内存级别的 VPS,更合适的方案是让 CI 完成构建,服务器只保存并提供静态文件。

这份案例描述本站的目标基础设施。当前仓库只包含网站源码,不代表示例域名、远程服务器和自动部署已经实际配置。

目标与约束

  • 生产环境没有 Node.js 常驻进程;
  • Caddy 负责 HTTPS、压缩与静态文件服务;
  • SELinux 保持 Enforcing,firewalld 保持启用;
  • 自动部署使用非 root 账户;
  • 现有代理服务可能占用 UDP/443,部署前必须确认端口;
  • 站点可以随时通过 Git 仓库重新构建。

技术架构

Git Repository
      │ push main

GitHub Actions
      │ npm ci / astro build

    dist/
      │ rsync over SSH

Rocky Linux /var/www/blog


    Caddy
      │ HTTPS

   Browser

构建产物和源代码有清晰边界。服务器上的 /var/www/blog 是可替换产物,Markdown、配置和构建逻辑只存在于版本库。

端口与现有服务

部署前先检查 TCP/80、TCP/443 和 UDP/443,而不是直接启动 Caddy:

sudo ss -lntup | grep -E ':(80|443)\b'

如果只有 UDP/443 被现有代理使用,可以让 Caddy 监听 TCP/80 和 TCP/443,并禁用 HTTP/3:

{
    servers :443 {
        protocols h1 h2
    }
}

如果 TCP/443 已被占用,则需要由一个明确的入口完成 SNI 或反向代理路由,不能让两个普通进程竞争同一监听地址。

Web 与安全边界

生产目录使用 /var/www/blog,Caddy 只读取静态文件。目录权限授予部署用户写入、Caddy 读取,不让自动部署账户拥有无关 sudo 权限。

SELinux 使用持久化文件上下文,而不是临时关闭:

sudo semanage fcontext -a -t httpd_sys_content_t "/var/www/blog(/.*)?"
sudo restorecon -Rv /var/www/blog

firewalld 只开放实际需要的服务。SSH 管理入口优先限制来源或通过 Tailscale 访问。

发布流程

自动化之前先手工跑通一次完整链路:

  1. 本地执行 npm cinpm run build
  2. dist/ 上传到临时目录;
  3. 同步到 /var/www/blog
  4. 使用 curl -I 检查 HTTPS 与缓存响应;
  5. 确认 Caddy 日志、文件权限与 SELinux 没有异常。

只有手工链路稳定后,才把同样步骤放进 CI。这样自动化失败时仍然知道每一层应该如何独立验证。

可恢复性

站点真正的源数据是 Git 仓库。服务器损坏时,新主机只需要恢复 Caddy 配置、安全策略和部署密钥,然后重新构建并同步 dist/node_modules 和服务器构建缓存都不属于备份对象。

项目复盘

低维护并不等于忽略安全,而是让组件数量与需求匹配。静态输出、最小权限和可重建部署将故障面缩小到 DNS、端口、TLS、文件权限和 Web Server 几个可观察层级。

真实上线前,需要替换示例域名,核对服务器端口占用,并在实际环境分别验证 firewalld、SELinux 与现有代理服务。