这次折腾的目标,是把一台 AWS EC2 实例改造成自己的多站点服务器:根域名运行 LunaTV,同时保留个人博客、静态博客、导航页和早期部署的 FishBlog。所有服务都通过 Docker Compose 管理,再由 Caddy 根据域名分流并自动申请 HTTPS 证书。
整个过程并非一次成功。期间遇到了公网 IP 变化、DNS 委派提示异常、证书申请时出现 NXDOMAIN、实例内存不足、旧服务清理以及域名重新分配等问题。这篇文章记录最终方案和主要排障过程,供以后重装或迁移时参考。
本文中的密码、SSH 私钥和恢复密钥均已省略。实际部署时不要把敏感信息直接写入公开仓库。
最终使用的环境如下:
t3.small3.22.30.132lalawatch.top最初实例使用的是动态公网 IP。实例重启或调整规格后,地址发生过变化,因此后来将 3.22.30.132 绑定为弹性 IP。对于需要长期绑定域名的服务器,弹性 IP 基本是必需项,否则每次 IP 变化都要重新修改 DNS。
SSH 使用 Ubuntu 默认用户和 AWS 下载的 PEM 密钥:
bashssh -i SSH.pem ubuntu@3.22.30.132
Windows 下如果 OpenSSH 认为私钥权限过宽,需要先收紧 PEM 文件的访问权限。EC2 安全组至少要允许:
部署期间 AWS 控制台曾提示 22 端口对所有 IPv4 地址开放。这样虽然连接方便,但风险较高,稳定后应改为仅允许可信来源。
域名购买于阿里云,权威 DNS 使用:
textdns29.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:
powershellResolve-DnsName nav.lalawatch.top -Type A -Server dns29.hichina.com
如果权威 DNS 已返回新记录,而本机仍显示 NXDOMAIN,通常只需等待递归 DNS 缓存过期。
所有服务加入同一个 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 的核心配置可以概括为:
caddyfilelalawatch.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/* 请求转发给后端服务,这里仅展示域名分流思路。
每次修改配置后,先验证再应用:
bashdocker compose config --quiet
docker exec caddy caddy validate --config /etc/caddy/Caddyfile
docker compose restart caddy
LunaTV 使用项目提供的容器镜像运行,并搭配 Kvrocks 保存配置和缓存。最初它使用 tv.lalawatch.top,后来重新规划域名,将 LunaTV 调整为主站:
texthttps://lalawatch.top
同时将应用内部的站点地址更新为根域名:
yamlenvironment:
SITE_BASE: https://lalawatch.top
旧的 tv.lalawatch.top 路由和 DNS 记录随后删除。
FishBlog 使用 fishblog_tpl 模板部署,完整运行需要多项依赖:
这也是整台服务器中资源占用最高的一组服务。最初的微型实例内存不足,部署和构建过程不稳定,因此将实例升级到 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 服务时仍需持续关注内存。
常用检查方式:
bashfree -h
docker stats --no-stream
df -h /
为了保留一套轻量静态博客,又部署了 Hugo,并创建私有仓库 zsoyph/myblog 保存博客源码。管理端使用 Sveltia CMS,内容更新后由 GitHub 工作流重新构建博客镜像。
Hugo 最初占用 blog.lalawatch.top,后来该域名让给 VanBlog,因此 Hugo 最终迁移到:
texthttps://hugo.lalawatch.top
Hugo 的运行容器只负责提供构建后的静态文件,实际内存占用很低,适合作为长期保留的备用博客或技术文档站。
中途还尝试过一套带管理后台的 Hexo 博客,并绑定 hexo.lalawatch.top。后续决定不再使用,于是进行了完整卸载:
local/hexo-blog:latest 镜像备份文件位于:
text/home/ubuntu/backups/hexo-blog-data-20260819-084429.tar.gz
卸载时先备份再删除,并对容器、镜像、数据卷、目录和配置引用逐项验证,避免误删其他站点的数据。
SimpleNav 是一个个性化导航服务。它曾短暂作为根域名主站,后来 LunaTV 恢复为主站,于是导航站迁移到:
texthttps://nav.lalawatch.top
与域名相关的运行变量也一并修改:
yamlenvironment:
NEXTAUTH_URL: https://nav.lalawatch.top
AZURE_REDIRECT_URI: https://nav.lalawatch.top/api/auth/callback
如果使用微软登录,还需要在 Azure 应用后台把新的回调地址加入允许列表,否则页面可以打开,但第三方登录会失败。
VanBlog 使用官方镜像,并配套独立的 MongoDB 4.4.16:
yamlservices:
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
数据分别保存到持久卷:
启动后的实际内存占用约为:
textVanBlog:约 234 MB MongoDB:约 107 MB
VanBlog 最初使用 vanblog.lalawatch.top,域名重新规划后迁移到:
texthttps://blog.lalawatch.top
初始化入口:
texthttps://blog.lalawatch.top/admin/init
应从最终域名进行初始化,否则内置图床可能生成错误的资源地址。旧的 vanblog.lalawatch.top 路由和 DNS 记录已经删除。
Caddy 可以自动向 Let's Encrypt 申请证书,但前提是公网 DNS 已经传播完成。部署中反复出现过以下错误:
textNXDOMAIN looking up A SERVFAIL looking up A Certificate not found
对应处理方式如下。
表示证书机构查询不到域名记录。先确认权威 DNS 是否已经返回正确 IP,不要连续重启服务碰运气。
Caddy 在失败后会指数退避,可能需要等待几分钟。确认 DNS 生效后,可以重启 Caddy 立即触发新一轮申请:
bashdocker compose restart caddy
在 Hugo 证书签发时,域名验证已经通过,但证书链下载短暂返回 404。重新启动 Caddy 后再次申请成功,说明这是证书机构的临时状态,不需要修改 DNS。
查看证书日志:
bashdocker logs --since 10m caddy 2>&1 | grep -E "certificate|challenge|域名"
最终 hugo.lalawatch.top 和 nav.lalawatch.top 均成功获得正式证书并返回 HTTP 200。
| 域名 | 服务 | 状态 |
|---|---|---|
lalawatch.top | LunaTV | 正常 |
blog.lalawatch.top | VanBlog | 正常 |
hugo.lalawatch.top | Hugo + Sveltia CMS | 正常 |
nav.lalawatch.top | SimpleNav | 正常 |
fishblog.lalawatch.top | FishBlog 前台 | 正常 |
admin.lalawatch.top | FishBlog 后台 | 正常 |
tv.lalawatch.top | 旧 LunaTV 地址 | 已停用 |
vanblog.lalawatch.top | 旧 VanBlog 地址 | 已停用 |
hexo.lalawatch.top | Hexo | 已卸载 |
每次修改 Compose 或 Caddy 配置前,都把旧文件复制到服务器的备份目录:
text/home/ubuntu/backups/
bashcd /home/ubuntu/fishblog-deploy
docker compose ps
bashdocker logs --tail 100 caddy
docker logs --tail 100 vanblog
docker logs --tail 100 moontv-core
bashdocker compose pull vanblog docker compose up -d vanblog
升级前应先备份对应的数据卷或数据库,不建议直接对整套服务执行无差别更新。
latest 更新带来的不确定性。最终方案的核心并不复杂:一个固定公网 IP、一组正确的 DNS 记录、一套统一的 Docker 网络,以及一个负责域名分流和 HTTPS 的 Caddy。真正耗时的部分主要是资源规划、DNS 缓存、证书申请重试和多个项目之间的域名迁移。
经过这一轮折腾,一台 t3.small 实例同时承载了 LunaTV、VanBlog、Hugo、SimpleNav 和 FishBlog。虽然资源已经比较紧张,但通过容器内存限制、持久卷和统一入口,整个结构仍然清晰,也为后续迁移、升级和备份留下了空间。
本文作者:oyph
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!