Coolify 中配置 Laravel Horizon:从安装、Supervisor 调优到 Dashboard 权限与告警排障的完整指南
导读
Horizon 是 Laravel 官方提供的 Redis 队列仪表盘与进程编排工具,它为基于 Redis 的队列提供实时监控、进程自动伸缩与失败任务管理能力。本指南以 Coolify 仓库中 .cursor/skills/configuring-horizon/SKILL.md 为核心,结合仓库内的 config/horizon.php、app/Providers/HorizonServiceProvider.php 与 app/Console/Kernel.php 等真实实现,系统讲解在 Laravel 应用(含 Coolify 这类自身重度依赖队列编排的 PaaS 项目)中安装、配置、运维 Horizon 的完整路径——从 supervisor 分块、队列优先级到 Dashboard 鉴权、LongWaitDetected 告警与空白的指标面板排障。
一、Horizon 是什么:Redis 队列驱动的完整生命周期
Horizon 不是一个通用的队列管理面板,它只针对 Redis 队列驱动生效,其核心职责是把"队列消费者进程"抽象为一组可声明式定义的 supervisor,并借助 Redis 本身保存 supervisor 列表、失败任务、任务指标等元信息(见 config/horizon.php 顶部注释中对 use Redis 连接的说明)。
在实际使用前应明确适用范围:
- 只有 queue driver 为
redis时才可用,数据库与 SQS 驱动均不支持; - Redis Cluster 不被支持,Horizon 要求独立(standalone)的 Redis 连接;
- 通用队列、独立 Redis 配置、Linux supervisord、Telescope、任务批处理等场景不应使用本技能。
在 Coolify 中,Horizon 是系统编排的基石:模型 ApplicationDeploymentQueue.php 代表了部署队列的持久化记录,而真正的异步执行体 ApplicationDeploymentJob.php 等大量任务由 Horizon 负责调度。
完整的生命周期链条
| 阶段 | 动作 | 说明 |
|---|---|---|
| 安装 | php artisan horizon:install |
生成配置文件与 ServiceProvider |
| 配置 | 编辑 config/horizon.php |
声明 supervisor、环境差异、告警阈值 |
| 鉴权 | 编辑 HorizonServiceProvider |
通过 Gate 限制 Dashboard 访问 |
| 运行 | php artisan horizon |
启动 master supervisor 及 workers |
| 指标 | 调度 horizon:snapshot |
每 5 分钟采集指标快照 |
| 排障 | 检查 timeout 链、告警、重启 | 结合 horizon:terminate 做优雅发布 |
二、安装与首启:horizon:install 之后做了什么
执行安装命令即可生成所需文件:
php artisan horizon:install
该命令会产生两个核心文件:
config/horizon.php—— 全部 supervisor、环境、告警、指标与裁剪配置;app/Providers/HorizonServiceProvider.php—— 负责注册 Horizon 路由中间件、定义 Dashboard 访问 Gate、以及注册自定义服务。
在 Coolify 中该 Provider 被充分自定义(app/Providers/HorizonServiceProvider.php):
- 在
register()中将JobRepository单例绑定到自研的CustomJobRepository(对应app/Repositories/CustomJobRepository.php与契约app/Contracts/CustomJobRepositoryInterface.php),实现对任务查询行为的定制; - 在
boot()中监听JobReserved事件,当ApplicationDeploymentJob被 worker 接管时,把 Horizon 的 job id 回写到对应ApplicationDeploymentQueue的horizon_job_id字段——这是 Coolify 在部署过程中定位"当前部署跑在哪个队列任务里"的关键机制; - 监听
JobFailed事件,在云端(isCloud())且失败原因为DeploymentException或TimeoutExceededException时从 Horizon 的 failed jobs 中主动移除该记录,避免无意义的失败展示(best-effort,不影响原始失败)。
三、Supervisor 配置:理解 defaults 与 environments 的合并语义
Horizon 的配置核心在 config/horizon.php 中的 defaults 与 environments 两个数组。
最关键的语义:environments 是对 defaults 的按 key 覆盖(merge),而不是整体替换。
'defaults' => [
'supervisor-1' => [
'connection' => 'redis',
'queue' => ['default'],
'balance' => 'auto',
'minProcesses' => 1,
'maxProcesses' => 10,
'tries' => 3,
],
],
'environments' => [
'production' => [
// 只覆盖这两个 key,其余继承 defaults
'supervisor-1' => ['maxProcesses' => 20, 'balanceCooldown' => 3],
],
'local' => [
'supervisor-1' => ['maxProcesses' => 2],
],
],
因此,把 connection、queue、balance、autoScalingStrategy、tries、timeout 等公共项写在 defaults,只在 production 里覆盖 maxProcesses、balanceMaxShift、balanceCooldown 是官方推荐的分层写法。
Coolify 的真实实践:环境变量驱动的 s6 supervisor
Coolify 并没有把整份配置硬编码,而是在 config/horizon.php 中用环境变量参数化了几乎每一个伸缩决策点:
'defaults' => [
's6' => [
'connection' => 'redis',
'balance' => env('HORIZON_BALANCE', 'false'),
'queue' => env('HORIZON_QUEUES', 'high,default'),
'maxTime' => env('HORIZON_MAX_TIME', 0),
'maxJobs' => 400,
'memory' => 128,
'tries' => 1,
'nice' => 0,
'sleep' => 3,
'timeout' => min(
max((int) env('HORIZON_TIMEOUT', 39600), ScheduledVolumeBackup::DEFAULT_TIMEOUT + 600),
85800,
),
],
],
'environments' => [
'production' => [
's6' => [
'autoScalingStrategy' => 'size',
'minProcesses' => env('HORIZON_MIN_PROCESSES', 1),
'maxProcesses' => env('HORIZON_MAX_PROCESSES', 4),
'balanceMaxShift' => env('HORIZON_BALANCE_MAX_SHIFT', 1),
'balanceCooldown' => env('HORIZON_BALANCE_COOLDOWN', 1),
],
],
],
几个值得注意的设计点:
- timeout 的钳制逻辑:
HORIZON_TIMEOUT默认 39600 秒(11 小时),但会被钳制到[模型定义的最大备份超时 + 600, 85800]区间内。这保证 Volume 备份这类超长任务(见 ScheduledVolumeBackup.php 的DEFAULT_TIMEOUT)不会被过于激进的 timeout 打断,同时也保证读源码的人能立刻看到"为什么 Coolify 部署 timeout 上限是 23.8 小时"。 - 默认关闭 auto-balance:
HORIZON_BALANCE默认false,意味着每个队列维持固定 worker 数量;需要按负载伸缩的部署环境再通过环境变量开启。 - production / local 当前配置相同,都预留了
autoScalingStrategy => 'size'与balanceMaxShift/balanceCooldown环境变量通道,便于集群内按节点规格调整。
队列优先级与伸缩策略的进阶用法
- 单 supervisor + auto balance 不保证队列顺序:
queue数组中的书写顺序在balance: auto下不用于负载分配。若要强制notifications优先于default,应拆分两个命名 supervisor——高优队列配更高的maxProcesses,低优队列压低上限。 balance: false固定 worker 数:对"同一时刻最多 N 个并发"的队列(例如仅允许 2 个并发的视频转码队列),应设balance: false+maxProcesses: 2,避免突发流量时 auto-balance 把并发抬高。balanceCooldown平滑突发伸缩:auto 模式下 supervisor 可能在突发负载时快速上蹿下跳,设置balanceCooldown(通常 3~5 秒)作为两次伸缩决策的最小间隔,并用balanceMaxShift限制单轮增删的进程数。
关键伸缩参数速查表
| 参数 | 作用 | Coolify 默认值(经环境变量) |
|---|---|---|
minProcesses |
每个队列维持的最小 worker 数 | HORIZON_MIN_PROCESSES,1 |
maxProcesses |
每个队列的最大 worker 数 | HORIZON_MAX_PROCESSES,4 |
balance |
auto/simple/false |
HORIZON_BALANCE,false |
balanceMaxShift |
单轮伸缩最多增删进程数 | HORIZON_BALANCE_MAX_SHIFT,1 |
balanceCooldown |
两次伸缩决策的最小间隔(秒) | HORIZON_BALANCE_COOLDOWN,1 |
tries |
任务失败前最大尝试次数 | 1(fail-fast 部署语义) |
timeout |
单个任务被判定超时的秒数 | 钳制于 39600~85800 |
maxJobs / maxTime |
worker 处理多少任务或存活多久后重启 | 400 / HORIZON_MAX_TIME,0 |
四、Dashboard 鉴权:用 Gate 锁住 /horizon
Horizon Dashboard 默认开放是危险行为,正确的做法是在 HorizonServiceProvider 的 gate() 中定义 viewHorizon。
技能文档给出的最小示例:
protected function gate(): void
{
Gate::define('viewHorizon', function (User $user) {
return $user->is_admin;
});
}
Coolify 的真实实现(app/Providers/HorizonServiceProvider.php)展示了更实际的授权策略——根用户(id === 0)直接放行,其余用户需命中 HORIZON_ALLOWED_EMAILS 白名单:
protected function gate(): void
{
Gate::define('viewHorizon', function (User $user) {
if ($user->id === 0) {
return true;
}
return str(config()->string('horizon.allowed_emails'))
->lower()
->explode(',')
->map(fn (string $email) => trim($email))
->contains($user->email);
});
}
对应的白名单来自 config/horizon.php 顶部配置:
'path' => env('HORIZON_PATH', 'horizon'), // Dashboard 路由前缀
'domain' => env('HORIZON_DOMAIN'), // 可选子域名,空则随主域
'allowed_emails' => env('HORIZON_ALLOWED_EMAILS', ''), // 逗号分隔的邮箱白名单
'prefix' => env('HORIZON_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_horizon:'),
这份配置还顺带说明:同一台服务器若跑多个 Laravel 应用实例,务必通过 HORIZON_PREFIX 区分各自在 Redis 中的数据,避免互相污染;middleware => ['web'] 表示所有 Horizon 路由都会挂载 web 中间件组。
五、指标面板:为什么 /horizon 的图是空的
指标面板空白的根因在 .cursor/skills/configuring-horizon/references/metrics.md 中被明确点出:
只运行
php artisan horizon不会自动填充指标。 指标图由快照构建,horizon:snapshot必须被调度执行(生产建议每 5 分钟一次)。
单次手动执行只能让面板"闪现"一次数据,无法持续更新,因此要在调度器里注册,而非手动跑。
Coolify 的做法(app/Console/Kernel.php)随运行环境区分频率:
if (isDev()) {
$this->scheduleInstance->command('horizon:snapshot')->everyMinute();
// ...
} else {
$this->scheduleInstance->command('horizon:snapshot')->everyFiveMinutes();
// ...
}
对应的保留策略在 config/horizon.php:
'metrics' => [
'trim_snapshots' => [
'job' => 24, // 保留最近 24 个 job 快照
'queue' => 24, // 保留最近 24 个 queue 快照
],
],
需要强调的陷阱:trim_snapshots 是快照条数而非时间长度。默认 24 条 × 5 分钟间隔 = 2 小时的指标历史;想保留更长时间就调大数值,代价是 Redis 占用上升。
同样值得关注的是 trim 数组——它按分钟控制任务在 Dashboard 中的留存:
| 配置 | 默认值 | 含义 |
|---|---|---|
trim.recent |
60 | 最近任务保留 60 分钟 |
trim.completed |
60 | 已完成任务保留 60 分钟 |
trim.recent_failed |
10080 | 最近失败任务保留 7 天 |
trim.failed |
10080 | 全部失败任务保留 7 天 |
trim.monitored |
10080 | 被监控任务保留 7 天 |
六、告警与通知:LongWaitDetected 的阈值到底在哪配
当任务在队列中等待超过阈值时,Horizon 会触发 LongWaitDetected 事件。关键结论(参考 .cursor/skills/configuring-horizon/references/notifications.md):
- 触发阈值不是配在通知路由里,而是在 config/horizon.php 的
waits数组,按connection:queue组合分别设定:
'waits' => [
'redis:default' => 60, // default 队列中的任务等待超过 60 秒即触发
],
如果告警太频繁或太迟钝,应调整 waits 而不是路由配置。例如要把高优队列的告警阈值收紧,可写成:
'waits' => [
'redis:high' => 15,
'redis:default' => 60,
],
- 事件被触发后,投递目标在
HorizonServiceProvider::boot()中声明,Horizon 已内置了LongWaitDetected → notification的接线,因此你只需要做路由而无需手写 listener:
Horizon::routeMailNotificationsTo('ops@example.com');
Horizon::routeSlackNotificationsTo('#horizon', 'token');
Horizon::routeSmsNotificationsTo('+14155550111');
- 失败任务告警不属于 Horizon 的通知 API。Horizon 文档化的通知只覆盖长等待类告警;如果还需要"任务失败就告警",应把它当作自定义的队列事件处理(监听
JobFailed),Coolify 的HorizonServiceProvider::boot()里就注册了这样的JobFailed监听,但那是为清理 failed 记录而非通用告警而设计的(参见上文第三节)。
七、Job 标签与静音:让 Dashboard 只展示有意义的内容
标签与静音的能力细节记录在 .cursor/skills/configuring-horizon/references/tags.md 中。
自动标签:Eloquent 模型任务无需写任何代码
只要 Job 的构造函数接收 Eloquent 模型实例,Horizon 就会自动以 ModelClass:id 格式打标签,例如 App\Models\User:42,Dashboard 中可直接按此过滤。只有在需要自定义标签(例如按业务维度过滤)时才需要给 Job 添加 tags() 方法。
Coolify 的 JobReserved 监听器正是依赖这一自动标签机制:它在 ApplicationDeploymentJob 的 payload 标签里寻找以 App\Models\ApplicationDeploymentQueue 开头的 tag,把冒号后的 id 解析出来回写部署队列记录。也就是说 Coolify 是在"自动标签"这个契约之上做的二次消费,自己并没有给每个 Job 手写 tags。
silenced:隐藏任务但不禁用任务
在 config/horizon.php 中:
'silenced' => [
// App\Jobs\ExampleJob::class,
],
把 Job 类加入 silenced 后,它会从 Dashboard 的 completed 列表中隐藏,但任务照常执行。它只是降低仪表盘噪声的工具,绝不是禁用任务的手段。
silenced_tags:按标签批量静音
任何携带匹配标签的任务都会从 completed 视图隐藏,适合按类别静音,例如一次静音所有带 notifications 标签的任务,而不是逐个列出类名。注意:参考文档提到 silenced_tags 是 Horizon 新版本才引入的配置项,使用前应针对当前 Horizon 版本查阅对应版本文档确认写法与默认值。
八、生产排障:timeout 链、重启策略与常见误区
timeout 链必须严格排序
任务、supervisor、Redis 三方超时之间存在一条必须遵守的顺序:
job timeout(任务自身的 timeout 配置)
< supervisor timeout(horizon.php defaults 中的 timeout)
< retry_after(config/queue.php 中 redis 连接的 retry_after)
顺序颠倒会导致任务在 Horizon 还未判定超时之前就被 Redis 重新入队,出现"任务一直重试"的假死循环。因此修改任何一环 timeout 前,都要回到 config/horizon.php 与 config/queue.php 核对三者的相对大小。
发布期优雅停止
horizon:terminate 会让 master supervisor 优雅地结束当前 worker 并不再取新任务,配合 horizon:snapshot 已调度的前提下,可实现零丢失发布。
Horizon 还支持通过 config/horizon.php 中的 fast_termination 加速发布流程:
'fast_termination' => false,
开启后 horizon:terminate 不会等待所有 worker 退出(除非显式加 --wait),允许新实例启动的同时旧实例继续收尾——这会显著缩短部署时的等待窗口,但代价是短暂时间内新旧两批 worker 并存。Coolify 默认关闭它,保持保守。
master supervisor 自身的健康边界
'memory_limit' => 64,
memory_limit 描述的是 Horizon master supervisor 进程可消耗的最大内存(MB),超过即被终止并重启。注意:worker 进程各自的内存上限不在这个字段控制,worker 侧的内存限制由 supervisor 块中的 memory 选项(Coolify 中为 128)控制。
常见误区清单
- ❌ 用 database / SQS 驱动运行 Horizon——不支持;
- ❌ 指望 Redis Cluster 环境跑 Horizon——不支持,必须是独立 Redis 连接;
- ❌ 在未读
config/horizon.php现状时就盲目改配置——先确认 supervisor 与 environment 的真实组成; - ❌ 假设
environments会整块替换defaults——它只覆盖你写出的 key; - ❌ 顺序搞反 timeout 链(job → supervisor → retry_after);
- ❌ 只跑
horizon却期待指标图自动出现——必须调度horizon:snapshot; - ❌ 把任务加入
silenced就以为任务不再执行——它仍然执行,只是不在列表显示。
九、验证清单与进阶阅读
配置完成后按如下清单验证:
- 运行
php artisan horizon并访问/horizon,确认 master supervisor 与各队列 worker 处于 running; - 确认 Dashboard 访问权限按预期收紧(root 或白名单邮箱之外的用户应被拒绝);
- 确认
horizon:snapshot已进入调度器并执行了一轮,指标图开始填充数据; - 压测观察
waits告警是否按阈值触发,若太频繁/太迟则回到config/horizon.php的waits调整。
参考文档对具体场景做了进一步的主题拆分,可按需查阅技能包内的原始资料:
- .cursor/skills/configuring-horizon/references/supervisors.md——supervisor 块、平衡策略、多队列与自动伸缩;
- .cursor/skills/configuring-horizon/references/notifications.md——
LongWaitDetected告警、路由与waits配置; - .cursor/skills/configuring-horizon/references/tags.md——任务标签、Dashboard 过滤与噪声任务静音;
- .cursor/skills/configuring-horizon/references/metrics.md——空白指标面板、快照调度与保留策略。
无论做何种调整,都要记住一个总原则:Horizon 的配置项名与默认值会随版本演进(例如 silenced_tags、autoScalingStrategy 等较新的选项),动手前先核对当前版本对应的官方文档,并以仓库内 config/horizon.php 的真实形态为基线——Coolify 这份配置本身就是一份"环境变量参数化 supervisor"的极佳范本,值得在自建 Laravel 应用时对照学习。
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 StartedRust0627
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