自托管服务实战(5):反向代理与自动 HTTPS(Caddy/Traefik)
前四篇的应用都优先绑定回环地址。本篇增加统一入口:由反向代理终止 TLS、按域名选择上游并记录请求结果。入口只负责传输与路由,不替代应用登录、主机防火墙或备份。
一、先画清一条请求链
请求依次经过 DNS、路由器或防火墙、代理监听端口、证书选择、路由规则和应用上游。浏览器报证书错误,应先核对域名、系统时间和证书链;返回 502,则通常是代理无法连接上游;返回应用自己的 401,反而说明前面的网络和 TLS 多半已经通了。按层定位比反复重启容器更可靠。
Caddy 适合用简洁配置自动申请证书;Traefik 擅长从容器标签动态发现路由。家庭环境选任何一个都可以,关键是只保留一个权威入口。代理与应用加入专用网络,数据库仍在内部网络。上游使用容器服务名和容器端口,不能把 localhost 误认为宿主机;在代理容器里,localhost 指代理自己。
公开域名申请证书通常需要 DNS 正确解析且验证端口可达,或使用 DNS-01。内网域名可使用私有 CA,但每台客户端必须信任根证书。不要跳过证书校验,也不要把私钥提交到 Git。证书状态目录需要持久化和备份,否则每次重建都重新申请,可能触发频率限制。
下面的程序静态审查一组路由:域名不能重复,上游不能指向数据库,公开入口必须要求 TLS。把路由清单转成这种中间表示,换 Caddy、Traefik 或其他代理时仍可复用规则。
from dataclasses import dataclass
@dataclass(frozen=True)
class Route:
host: str
upstream: str
tls: bool
public: bool
routes = [
Route("vault.example.net", "vaultwarden:80", True, True),
Route("notes.example.net", "knowledge:3000", True, True),
Route("status.home.arpa", "uptime:3001", True, False),
]
seen = set()
errors = []
for route in routes:
valid_host = route.host not in seen
valid_tls = route.tls or not route.public
safe_upstream = not route.upstream.startswith(("postgres:", "redis:"))
ok = valid_host and valid_tls and safe_upstream
print(f"{route.host}: {'PASS' if ok else 'FAIL'} -> {route.upstream}")
if not ok:
errors.append(route.host)
seen.add(route.host)
print(f"unique_hosts={len(seen)}")
print(f"route_gate={'PASS' if not errors else 'FAIL'}")
运行输出:
vault.example.net: PASS -> vaultwarden:80
notes.example.net: PASS -> knowledge:3000
status.home.arpa: PASS -> uptime:3001
unique_hosts=3
route_gate=PASS
二、自动 HTTPS 也需要验收
自动续期并不意味着永不失效。监控应从客户端视角读取证书的域名、签发者和到期时间,并在剩余 30 天、14 天等阈值告警。代理健康检查只证明进程活着;还应通过域名访问一个受控路径,验证 DNS、TLS、路由与上游整体链路。日志保留状态码、耗时和请求 ID即可,密码、Cookie、授权头与查询中的令牌必须脱敏。
以下程序计算证书剩余天数并给出分级结论,使用固定时间保证输出可复现。生产脚本可将 expires 替换为 TLS 握手读到的日期。
from datetime import datetime, timezone
now = datetime(2026, 8, 4, tzinfo=timezone.utc)
certificates = {
"vault.example.net": datetime(2026, 10, 20, tzinfo=timezone.utc),
"notes.example.net": datetime(2026, 9, 18, tzinfo=timezone.utc),
"status.home.arpa": datetime(2027, 8, 4, tzinfo=timezone.utc),
}
states = []
for host, expires in certificates.items():
days = (expires - now).days
if days < 14:
state = "critical"
elif days < 30:
state = "warning"
else:
state = "ok"
states.append(state)
print(f"{host}: days_left={days} state={state}")
healthy = all(state == "ok" for state in states)
print(f"checked={len(certificates)}")
print(f"certificate_gate={'PASS' if healthy else 'FAIL'}")
运行输出:
vault.example.net: days_left=77 state=ok
notes.example.net: days_left=45 state=ok
status.home.arpa: days_left=365 state=ok
checked=3
certificate_gate=PASS
三、上线与迁移顺序
先在本机 hosts 文件或测试子域验证新代理,不立即改生产 DNS。确认响应头、上传大小、WebSocket、流式请求和真实客户端 IP;不同应用对这些能力要求不同。再把 DNS TTL 临时降低,切换入口并观察错误率。旧入口保留短时间只读回退,稳定后关闭多余端口。
代理配置纳入版本控制,证书私钥与 DNS API 令牌不纳入。DNS 令牌只授予所需区域和记录权限。若使用容器自动发现,不要默认暴露所有容器;采用显式启用标签,且谨慎授予 Docker socket,因为它拥有很高的宿主控制能力。
上线后还应保存一份绕过代理的上游探测方法。外部域名失败而上游正常,问题在 DNS、TLS 或代理;两者同时失败,才继续检查应用和数据层。多域名共用入口时,为每个上游设置独立超时与请求体上限,避免一个慢服务耗尽代理连接,也避免上传型应用被默认限制意外截断。变更配置前运行语法校验,加载后确认旧连接如何处理,才能做到平滑而非碰运气式重启。
可迁移的成果是域名到上游的路由表、证书获取方式、秘密恢复位置与端到端探测,而非某个代理品牌。下一篇在此基础上讨论远程访问:对管理界面和家庭服务,私有组网往往比把更多域名暴露到公网更合适。
参考来源
👍 觉得有用就点个 赞 + 收藏,方便回头查阅;有疑问直接在评论区留言,我看到都会回。
🚀 本文属于 《自托管服务实战》 系列,持续更新,关注不迷路。
📌 文章里的代码都能直接跑。想要可直接 clone 的完整工程 + 配套部署脚本 / 踩坑清单?评论一声或发邮件到 cj2664@qq.com,我免费发你。
如果你正好在做类似系统、或有工程化难题想找人做,也欢迎邮件聊一句——我按实际情况评估,能落地的就接单或出方案。评论和邮件都能直接找到我,不用跳别的平台。
更多推荐


所有评论(0)