Coolify 外部 TLS 下的 HTTP→HTTPS 重定向设计:破除 Cloudflare Tunnel 重定向死循环
导读
当 Coolify 通过 Cloudflare Tunnel 的 "all-resource" 模式对外提供服务时,公网 HTTPS 请求实际是以 http://localhost:80 进入 Coolify 代理的;若资源域名同时以 https:// 存储且 Coolify 侧强制 HTTP→HTTPS 跳转,请求会在隧道入口反复打回,最终报 TOO_MANY_REDIRECTS。本设计文档方案引入可开关的 Redirect HTTP to HTTPS 控制项,使 Coolify 能够如实以 https:// 保存对外域名、把 TLS 终结与 80→443 跳转交给 Cloudflare 等上游反代处理,并统一覆盖普通应用与 Service 应用。读完本文,你将掌握该开关的数据模型、Traefik/Caddy 代理标签生成差异、UI 行为与自动化测试边界,可直接对照本仓库源码逐行验证。
问题背景:HTTP 入口上叠加强制 HTTPS 的恶性循环
在 Cloudflare Tunnel 的 all-resource 场景下,Coolify 的整个服务器由一个隧道统一接入。cloudflared 对公网终结 TLS 后,把解密流量转发给 Coolify 的 HTTP 入口(http://localhost:80)。此时若某个资源的域名在 Coolify 里被保存成 https://app.example.com,而 Coolify 又为该域名生成了 HTTP→HTTPS 重定向,就会形成如下死循环:
- 用户访问
https://app.example.com→ Cloudflare 完成 TLS 终结; - 流量以
http://app.example.com打回 Coolify 的:80入口; - Coolify 看到请求走的是 HTTP 路由,于是 301 跳转到
https://app.example.com; - 跳转结果再次回到 Cloudflare 公网入口 → 再次以
http://打进:80……周而复始,直到浏览器报TOO_MANY_REDIRECTS。
旧文档给出的规避办法是“把域名存成 http://”,但这掩盖了真实公网地址,会引发连锁副作用:
- 服务端生成的 Secure Cookie 语义错误(HTTPS 页面上的 http 源 URL 可能导致 cookie 判定异常);
- OAuth 回调 URL、canonical link 等依赖真实 scheme 的机制被误导。
原有的旁路手段并不完整:普通应用虽可在 Advanced 设置里关闭强制 HTTPS(见 app/Livewire/Project/Application/Advanced.php),但该开关离域名配置很远、不易发现;而 Service 应用生成的代理配置则始终硬编码启用该跳转,根本没有关闭入口。
设计目标与明确的非目标
目标:
- 以
https://准确存储对外可见的域名; - 让 Cloudflare 这类上游反代负责 HTTP→HTTPS 跳转与 TLS 终结;
- 行为对普通应用与 Service 应用保持一致;
- 默认不改变存量资源的既有安全行为;
- 保持通用性:不绑定 Cloudflare,也不引入服务器级 all-resource 隧道模式。
明确不做的事(避免过度设计):
- 不自动探测 Cloudflare;
- 不新增服务器级 all-resource 隧道开关;
- 不配置可信转发头网络(trusted forwarded-header networks);
- 不取代 end-to-end origin TLS 的工作流;
- 不改变新旧资源的默认跳转行为。
数据模型:Service 应用新增布尔列,普通应用复用旧设置
设计方案为 Service 应用(service_applications 表)新增非空布尔列 is_force_https_enabled,默认 true,从而保证迁移后存量 Service 应用行为不变。对应的迁移文件已落地:
database/migrations/2026_08_17_000000_add_is_force_https_enabled_to_service_applications_table.php:
public function up(): void
{
Schema::table('service_applications', function (Blueprint $table) {
$table->boolean('is_force_https_enabled')->default(true);
});
}
public function down(): void
{
Schema::table('service_applications', function (Blueprint $table) {
$table->dropColumn('is_force_https_enabled');
});
}
在 app/Models/ServiceApplication.php 中,该字段已进入 $fillable(第 35 行)、声明 $attributes 默认值 true(第 50 行)、并注册 boolean 强转(第 58 行),同时新增了与普通应用同名的访问器:
public function isForceHttpsEnabled(): bool
{
return $this->is_force_https_enabled;
}
从源码结构看,Service 应用与普通应用因此统一了读取入口:代理标签生成代码无需关心资源类型,只要调用 isForceHttpsEnabled() 即可。
普通应用不新增任何列,继续沿用既有的 ApplicationSetting::is_force_https_enabled(见 app/Models/ApplicationSetting.php 中的 cast 与 fillable 声明、app/Models/Application.php 的读取方法),也不存储任何与 Cloudflare 相关的专有状态。
UI 行为:域名页的 Redirect HTTP to HTTPS 开关
当资源至少有一个 https:// 域名时,Domains 页面会展示名为 Redirect HTTP to HTTPS 的布尔开关:
-
默认开启,帮助文本为:
Disable this when HTTPS and redirects are handled by Cloudflare Tunnel or another reverse proxy that connects to Coolify over HTTP.
-
处于 Cloudflare Tunnel 场景的用户配置
https://app.example.com后关闭此开关;直接暴露资源的用户保持开启。
针对不同类型资源,开关的落点不同:
| 资源类型 | 开关绑定的存储位置 | 备注 |
|---|---|---|
| 普通应用 / Docker Compose 应用 | 既有 ApplicationSetting::is_force_https_enabled |
Advanced 页同名旧开关不再作为独立事实源,应删除或改为同一设置并换用更清晰的文案 |
| Service 应用中的每个 application service | 新增的 service_applications.is_force_https_enabled |
每个 application service 各有一个开关 |
| 仅含数据库的 Service 条目 | 不展示该开关 | 数据库域名不涉及 HTTP 跳转中间件 |
UI 实现可在 Service 应用的 Domains 视图中看到该控件:label="Redirect HTTP to HTTPS" 并绑定 updateForceHttps 变更处理器(见 resources/views/livewire/project/service/domains.blade.php),对应 Livewire 交互位于 app/Livewire/Project/Service/Domains.php 与 app/Livewire/Project/Application/Domains.php。
纯 HTTP 资源不展示该开关,且隐藏它不会重置已存储的值。测试 tests/Feature/ServiceDomainsTest.php 同时覆盖了“存在 HTTPS 域名时渲染开关”“HTTP-only 资源不渲染开关”两个分支。
代理配置:域名 scheme 与跳转策略彻底解耦
设计核心原则是域名书写方式(scheme)与重定向策略相互独立:
https://域名照常生成 HTTPS 路由/监听器;- 其 HTTP 路由/监听器始终一并生成;
- 开启跳转时,HTTP 路由挂上 HTTPS 重定向中间件;
- 关闭跳转时,HTTP 路由直接转发到资源、不带该中间件。
两者都由 bootstrap/helpers/docker.php 的标签生成函数承接。下面分别看 Traefik 与 Caddy 两个代理的实现差异。
Traefik:HTTP 路由的中间件链按开关分支
fqdnLabelsForTraefik 接收 bool $is_force_https_enabled = false。对 https:// 域名,它总是:
- 先定义全局
redirect-to-https中间件(第 610 行):traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https; - 生成 HTTPS 路由器(entryPoints=https,开启 TLS、letsencrypt certresolver,第 697-768 行);
- 生成 HTTP 路由器(entryPoints=http),并在其中按开关决定中间件链(第 771-788 行):
if ($is_force_https_enabled) {
$httpMiddlewares = collect([]);
if ($is_noindex) {
$httpMiddlewares->push($noindex_name);
}
$httpMiddlewares->push('redirect-to-https');
} else {
$httpMiddlewares = $middlewares;
}
if ($httpMiddlewares->isNotEmpty()) {
$labels->push("traefik.http.routers.{$http_label}.middlewares={$httpMiddlewares->join(',')}");
}
关键差异在于:关闭跳转后,HTTP 路由直接复用 HTTPS 路由已算好的 $middlewares(含 gzip、stripprefix、www/non-www、basic auth、noindex 及用户自定义 service labels 中解析出的中间件),唯独不再追加 redirect-to-https。这与“只移除跳转行为、其余中间件保留”的测试要求一一对应(参见 tests/Unit/FqdnLabelsNoindexTest.php)。
Caddy:仅暴露 http:// + https:// 双站点、跳转目标跟随开关
fqdnLabelsForCaddy 对 Caddy 的处理是改变监听站点声明:当域名是 https:// 且关闭强制跳转时,站点地址不再是单一的 https://host,而是同时监听两个 scheme(第 554-556 行):
if ($schema === 'https' && ! $is_force_https_enabled) {
$siteAddress = "http://{$host}, https://{$host}";
}
同时,Caddy 中的 www / non-www 重定向目标也以开关驱动(第 588-593 行):$redirect_schema = $is_force_https_enabled ? $schema : '{scheme}'。也就是说关闭强制 HTTPS 后,www 跳转保留原请求 scheme({scheme}),不会把 HTTP 流量强行抬升到 HTTPS。反向代理(reverse_proxy)、gzip 编码、noindex 头部、basic auth 等中间件照旧生成,行为不受影响。
Service 应用:用存储值替换硬编码的 true
原先 Service 应用在生成 Traefik/Caddy 标签时把 is_force_https_enabled 写死为 true。现在改为传入 ServiceApplication::isForceHttpsEnabled 的存储值,使域名页开关真正作用到标签上。在 bootstrap/helpers/docker.php 等聚合生成处,也能看到普通应用以 $application->isForceHttpsEnabled() 传入相同参数的对称写法。
Preview 部署继承父应用既有的跳转设置,与当前普通应用行为保持一致(预览应用拥有独立的 ApplicationSetting 记录、在创建时复制父应用配置,因此继承自然成立)。
校验、授权与保存流程
- Service 应用的新值按布尔值校验;更新走与其它 Service 域名设置相同的授权检查(参见 app/Actions/Service/UpdateServiceApplicationFromApi.php 与 API 控制器 app/Http/Controllers/Api/ServiceApplicationsController.php 中的同名处理)。
- 变更该值会将代理配置标记为已变更(
config_hash失效),并走域名配置既有的 save/redeploy 流程,改动才会真正同步到代理端。 - 只有当 HTTPS 域名存在时开关才相关;HTTP-only 资源不展示,也不重置存储值。
面向用户的迁移与使用流程
更新后的 Cloudflare all-resource 部署指南建议用户按以下三步配置:
- 用
https://保存资源的公网域名(如实记录对外 URL); - 在 Domains 页为该资源关闭 Redirect HTTP to HTTPS;
- 让 Cloudflare 负责公网跳转与 TLS 终结。
指南同时保留完整 TLS 方案作为备选:需要 cloudflared 与 Coolify 的 HTTPS 入口之间走 TLS 的用户,可继续使用 origin TLS 工作流,而不必关闭此开关。
手工冒烟验证步骤(设计文档推荐):
- 将一个 Cloudflare Tunnel 主机名指向
http://localhost:80; - 在 Coolify 中把资源域名保存为
https://; - 关闭该资源的 Redirect HTTP to HTTPS;
- 用浏览器访问公网 HTTPS URL,确认页面正常加载、不再出现重定向死循环。
测试矩阵:自动化覆盖点
设计与实现对应的自动化测试需覆盖:
| 场景 | 断言要点 |
|---|---|
| 普通应用 HTTPS 域名 + 开关开启/关闭 | 对应跳转中间件存在/不存在 |
| Service 应用 HTTPS 域名 + 开关开启/关闭 | 同上,且通过 Service 侧 UI 持久化 |
| Service 应用默认值 | 迁移/新建后保持启用 |
| Traefik 与 Caddy | 关闭时仅省略跳转行为,gzip、stripprefix、auth、noindex、www/non-www 等中间件完整保留 |
| HTTP-only 资源 | UI 不显示无关开关(见 tests/Feature/ServiceDomainsTest.php) |
| Domains UI 持久化 | 遵循既有授权规则(见 tests/Feature/ApplicationDomainsTest.php) |
兼容性与安全
- 数据库列默认值为
true,保证 Service 应用存量行为不回归;普通应用既有值原样保留。 - 不做自动迁移推断:不会猜测哪些资源位于 Cloudflare 之后,避免把默认行为静默改写为不安全的直连转发。
需要深入阅读源码时,可依次对照:设计文档 docs/superpowers/specs/2026-08-17-external-tls-http-redirect-design.md、标签生成核心 bootstrap/helpers/docker.php、模型定义 app/Models/ServiceApplication.php 与 app/Models/ApplicationSetting.php、迁移文件 2026_08_17_000000_add_is_force_https_enabled_to_service_applications_table.php,以及上述 Feature/Unit 测试。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0626
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00