编辑
2026-08-19
瞎折腾
00

目录

从零到多站点:lalawatch.top 的 AWS EC2 折腾记录
一、服务器与基础环境
二、DNS 配置与第一次踩坑
三、统一入口:Docker Compose 与 Caddy
四、部署 LunaTV
五、部署 FishBlog
六、Hugo、Sveltia CMS 与 GitHub
七、部署并卸载 Hexo
八、部署 SimpleNav
九、部署 VanBlog
十、HTTPS 证书排障
1. NXDOMAIN
2. DNS 已生效但 Caddy 仍未申请
3. Certificate not found
十一、最终域名分配
十二、备份与日常维护
配置备份
查看服务状态
查看异常日志
更新单个服务
后续建议
总结

从零到多站点:lalawatch.top 的 AWS EC2 折腾记录

这次折腾的目标,是把一台 AWS EC2 实例改造成自己的多站点服务器:根域名运行 LunaTV,同时保留个人博客、静态博客、导航页和早期部署的 FishBlog。所有服务都通过 Docker Compose 管理,再由 Caddy 根据域名分流并自动申请 HTTPS 证书。

整个过程并非一次成功。期间遇到了公网 IP 变化、DNS 委派提示异常、证书申请时出现 NXDOMAIN、实例内存不足、旧服务清理以及域名重新分配等问题。这篇文章记录最终方案和主要排障过程,供以后重装或迁移时参考。

本文中的密码、SSH 私钥和恢复密钥均已省略。实际部署时不要把敏感信息直接写入公开仓库。

一、服务器与基础环境

最终使用的环境如下:

  • 云平台:AWS EC2
  • 区域:美国东部(俄亥俄)
  • 实例规格:t3.small
  • 系统:Ubuntu Server
  • 系统盘:50 GB
  • 弹性公网 IP:3.22.30.132
  • 域名:lalawatch.top
  • DNS:阿里云云解析 DNS
  • 容器管理:Docker Compose
  • 入口网关:Caddy 2

最初实例使用的是动态公网 IP。实例重启或调整规格后,地址发生过变化,因此后来将 3.22.30.132 绑定为弹性 IP。对于需要长期绑定域名的服务器,弹性 IP 基本是必需项,否则每次 IP 变化都要重新修改 DNS。

SSH 使用 Ubuntu 默认用户和 AWS 下载的 PEM 密钥:

bash
ssh -i SSH.pem ubuntu@3.22.30.132

Windows 下如果 OpenSSH 认为私钥权限过宽,需要先收紧 PEM 文件的访问权限。EC2 安全组至少要允许:

  • TCP 22:SSH,建议仅允许自己的公网 IP
  • TCP 80:HTTP
  • TCP 443:HTTPS
  • UDP 443:HTTP/3,可选

部署期间 AWS 控制台曾提示 22 端口对所有 IPv4 地址开放。这样虽然连接方便,但风险较高,稳定后应改为仅允许可信来源。

二、DNS 配置与第一次踩坑

域名购买于阿里云,权威 DNS 使用:

text
dns29.hichina.com dns30.hichina.com

刚添加根域名 A 记录时,云解析页面一度提示注册商 NS 与云解析 NS 不一致。域名详情页实际已经显示正确的两个 DNS 服务器,问题主要来自新注册域名的委派传播与缓存延迟。

最终所有有效 A 记录均指向弹性 IP:

text
@ A 3.22.30.132 blog A 3.22.30.132 hugo A 3.22.30.132 nav A 3.22.30.132 fishblog A 3.22.30.132 admin A 3.22.30.132

判断 DNS 是否真正生效时,不应只看本机缓存。可以直接查询权威 DNS:

powershell
Resolve-DnsName nav.lalawatch.top -Type A -Server dns29.hichina.com

如果权威 DNS 已返回新记录,而本机仍显示 NXDOMAIN,通常只需等待递归 DNS 缓存过期。

三、统一入口:Docker Compose 与 Caddy

所有服务加入同一个 Docker 网络,只有 Caddy 对宿主机暴露 80 和 443 端口。其他容器只在内部网络提供服务,这样可以减少端口冲突和不必要的公网暴露。

最终访问结构如下:

flowchart TD
    U["浏览器"] --> D["阿里云 DNS"]
    D --> C["Caddy :80 / :443"]
    C --> L["lalawatch.top → LunaTV"]
    C --> V["blog.lalawatch.top → VanBlog"]
    C --> H["hugo.lalawatch.top → Hugo"]
    C --> N["nav.lalawatch.top → SimpleNav"]
    C --> F["fishblog.lalawatch.top → FishBlog"]
    C --> A["admin.lalawatch.top → FishBlog Admin"]

Caddy 的核心配置可以概括为:

caddyfile
lalawatch.top { reverse_proxy moontv-core:3000 } blog.lalawatch.top { reverse_proxy vanblog:80 } hugo.lalawatch.top { reverse_proxy hugo-blog:80 } nav.lalawatch.top { reverse_proxy simplenav:3000 } fishblog.lalawatch.top { reverse_proxy fishblog-web:80 } admin.lalawatch.top { reverse_proxy fishblog-admin:80 }

实际 FishBlog 还需要把 /api/* 请求转发给后端服务,这里仅展示域名分流思路。

每次修改配置后,先验证再应用:

bash
docker compose config --quiet docker exec caddy caddy validate --config /etc/caddy/Caddyfile docker compose restart caddy

四、部署 LunaTV

LunaTV 使用项目提供的容器镜像运行,并搭配 Kvrocks 保存配置和缓存。最初它使用 tv.lalawatch.top,后来重新规划域名,将 LunaTV 调整为主站:

text
https://lalawatch.top

同时将应用内部的站点地址更新为根域名:

yaml
environment: SITE_BASE: https://lalawatch.top

旧的 tv.lalawatch.top 路由和 DNS 记录随后删除。

五、部署 FishBlog

FishBlog 使用 fishblog_tpl 模板部署,完整运行需要多项依赖:

  • MySQL 8
  • Redis
  • RabbitMQ
  • Elasticsearch 7.9.3
  • Java 后端
  • 博客前端
  • 管理后台

这也是整台服务器中资源占用最高的一组服务。最初的微型实例内存不足,部署和构建过程不稳定,因此将实例升级到 t3.small,并把系统盘扩展到 50 GB。

最终地址:

text
博客前台:https://fishblog.lalawatch.top 管理后台:https://admin.lalawatch.top

为适应 2 GB 内存,给各容器设置了内存上限,并限制 Elasticsearch JVM 堆、MySQL 缓冲池和 Java 后端堆大小。服务器还配置了 2 GB Swap,但运行多个 Java、数据库和 Node.js 服务时仍需持续关注内存。

常用检查方式:

bash
free -h docker stats --no-stream df -h /

六、Hugo、Sveltia CMS 与 GitHub

为了保留一套轻量静态博客,又部署了 Hugo,并创建私有仓库 zsoyph/myblog 保存博客源码。管理端使用 Sveltia CMS,内容更新后由 GitHub 工作流重新构建博客镜像。

Hugo 最初占用 blog.lalawatch.top,后来该域名让给 VanBlog,因此 Hugo 最终迁移到:

text
https://hugo.lalawatch.top

Hugo 的运行容器只负责提供构建后的静态文件,实际内存占用很低,适合作为长期保留的备用博客或技术文档站。

七、部署并卸载 Hexo

中途还尝试过一套带管理后台的 Hexo 博客,并绑定 hexo.lalawatch.top。后续决定不再使用,于是进行了完整卸载:

  • 删除 Hexo 容器
  • 删除 local/hexo-blog:latest 镜像
  • 删除 Hexo 数据卷
  • 删除服务器项目目录
  • 删除 Caddy 中的 Hexo 路由
  • 保留卸载前的数据备份

备份文件位于:

text
/home/ubuntu/backups/hexo-blog-data-20260819-084429.tar.gz

卸载时先备份再删除,并对容器、镜像、数据卷、目录和配置引用逐项验证,避免误删其他站点的数据。

八、部署 SimpleNav

SimpleNav 是一个个性化导航服务。它曾短暂作为根域名主站,后来 LunaTV 恢复为主站,于是导航站迁移到:

text
https://nav.lalawatch.top

与域名相关的运行变量也一并修改:

yaml
environment: NEXTAUTH_URL: https://nav.lalawatch.top AZURE_REDIRECT_URI: https://nav.lalawatch.top/api/auth/callback

如果使用微软登录,还需要在 Azure 应用后台把新的回调地址加入允许列表,否则页面可以打开,但第三方登录会失败。

九、部署 VanBlog

VanBlog 使用官方镜像,并配套独立的 MongoDB 4.4.16:

yaml
services: vanblog-mongo: image: mongo:4.4.16 mem_limit: 384m vanblog: image: mereith/van-blog:latest environment: VAN_BLOG_DATABASE_URL: mongodb://vanblog-mongo:27017/vanBlog?authSource=admin mem_limit: 512m

数据分别保存到持久卷:

  • MongoDB 数据
  • 内置图床文件
  • 日志
  • VanBlog 内部 Caddy 配置与数据

启动后的实际内存占用约为:

text
VanBlog:约 234 MB MongoDB:约 107 MB

VanBlog 最初使用 vanblog.lalawatch.top,域名重新规划后迁移到:

text
https://blog.lalawatch.top

初始化入口:

text
https://blog.lalawatch.top/admin/init

应从最终域名进行初始化,否则内置图床可能生成错误的资源地址。旧的 vanblog.lalawatch.top 路由和 DNS 记录已经删除。

十、HTTPS 证书排障

Caddy 可以自动向 Let's Encrypt 申请证书,但前提是公网 DNS 已经传播完成。部署中反复出现过以下错误:

text
NXDOMAIN looking up A SERVFAIL looking up A Certificate not found

对应处理方式如下。

1. NXDOMAIN

表示证书机构查询不到域名记录。先确认权威 DNS 是否已经返回正确 IP,不要连续重启服务碰运气。

2. DNS 已生效但 Caddy 仍未申请

Caddy 在失败后会指数退避,可能需要等待几分钟。确认 DNS 生效后,可以重启 Caddy 立即触发新一轮申请:

bash
docker compose restart caddy

3. Certificate not found

在 Hugo 证书签发时,域名验证已经通过,但证书链下载短暂返回 404。重新启动 Caddy 后再次申请成功,说明这是证书机构的临时状态,不需要修改 DNS。

查看证书日志:

bash
docker logs --since 10m caddy 2>&1 | grep -E "certificate|challenge|域名"

最终 hugo.lalawatch.topnav.lalawatch.top 均成功获得正式证书并返回 HTTP 200。

十一、最终域名分配

域名服务状态
lalawatch.topLunaTV正常
blog.lalawatch.topVanBlog正常
hugo.lalawatch.topHugo + Sveltia CMS正常
nav.lalawatch.topSimpleNav正常
fishblog.lalawatch.topFishBlog 前台正常
admin.lalawatch.topFishBlog 后台正常
tv.lalawatch.top旧 LunaTV 地址已停用
vanblog.lalawatch.top旧 VanBlog 地址已停用
hexo.lalawatch.topHexo已卸载

十二、备份与日常维护

配置备份

每次修改 Compose 或 Caddy 配置前,都把旧文件复制到服务器的备份目录:

text
/home/ubuntu/backups/

查看服务状态

bash
cd /home/ubuntu/fishblog-deploy docker compose ps

查看异常日志

bash
docker logs --tail 100 caddy docker logs --tail 100 vanblog docker logs --tail 100 moontv-core

更新单个服务

bash
docker compose pull vanblog docker compose up -d vanblog

升级前应先备份对应的数据卷或数据库,不建议直接对整套服务执行无差别更新。

后续建议

  1. 将 SSH 端口限制为可信 IP,避免长期对公网开放。
  2. 定期备份 MongoDB、MySQL、Hugo 源码和图床文件。
  3. 监控内存与 Swap;如果 Swap 长期高占用,应继续升级实例或减少常驻服务。
  4. 为 Docker 日志配置轮转,避免日志长期占满磁盘。
  5. 为关键服务固定镜像版本,减少 latest 更新带来的不确定性。
  6. 如果 SimpleNav 使用 Azure 登录,记得同步更新 OAuth 回调地址。

总结

最终方案的核心并不复杂:一个固定公网 IP、一组正确的 DNS 记录、一套统一的 Docker 网络,以及一个负责域名分流和 HTTPS 的 Caddy。真正耗时的部分主要是资源规划、DNS 缓存、证书申请重试和多个项目之间的域名迁移。

经过这一轮折腾,一台 t3.small 实例同时承载了 LunaTV、VanBlog、Hugo、SimpleNav 和 FishBlog。虽然资源已经比较紧张,但通过容器内存限制、持久卷和统一入口,整个结构仍然清晰,也为后续迁移、升级和备份留下了空间。

本文作者:oyph

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!