首页
/ Coolify 中配置 Laravel Horizon:从安装、Supervisor 调优到 Dashboard 权限与告警排障的完整指南

Coolify 中配置 Laravel Horizon:从安装、Supervisor 调优到 Dashboard 权限与告警排障的完整指南

2026-09-07 10:56:49作者:董宙帆

导读

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):

  1. register() 中将 JobRepository 单例绑定到自研的 CustomJobRepository(对应 app/Repositories/CustomJobRepository.php 与契约 app/Contracts/CustomJobRepositoryInterface.php),实现对任务查询行为的定制;
  2. boot() 中监听 JobReserved 事件,当 ApplicationDeploymentJob 被 worker 接管时,把 Horizon 的 job id 回写到对应 ApplicationDeploymentQueuehorizon_job_id 字段——这是 Coolify 在部署过程中定位"当前部署跑在哪个队列任务里"的关键机制;
  3. 监听 JobFailed 事件,在云端(isCloud())且失败原因为 DeploymentExceptionTimeoutExceededException 时从 Horizon 的 failed jobs 中主动移除该记录,避免无意义的失败展示(best-effort,不影响原始失败)。

三、Supervisor 配置:理解 defaultsenvironments 的合并语义

Horizon 的配置核心在 config/horizon.php 中的 defaultsenvironments 两个数组。

最关键的语义: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],
    ],
],

因此,把 connectionqueuebalanceautoScalingStrategytriestimeout 等公共项写在 defaults,只在 production 里覆盖 maxProcessesbalanceMaxShiftbalanceCooldown 是官方推荐的分层写法。

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.phpDEFAULT_TIMEOUT)不会被过于激进的 timeout 打断,同时也保证读源码的人能立刻看到"为什么 Coolify 部署 timeout 上限是 23.8 小时"。
  • 默认关闭 auto-balanceHORIZON_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 默认开放是危险行为,正确的做法是在 HorizonServiceProvidergate() 中定义 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.phpwaits 数组,按 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.phpconfig/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 就以为任务不再执行——它仍然执行,只是不在列表显示。

九、验证清单与进阶阅读

配置完成后按如下清单验证:

  1. 运行 php artisan horizon 并访问 /horizon,确认 master supervisor 与各队列 worker 处于 running;
  2. 确认 Dashboard 访问权限按预期收紧(root 或白名单邮箱之外的用户应被拒绝);
  3. 确认 horizon:snapshot 已进入调度器并执行了一轮,指标图开始填充数据;
  4. 压测观察 waits 告警是否按阈值触发,若太频繁/太迟则回到 config/horizon.phpwaits 调整。

参考文档对具体场景做了进一步的主题拆分,可按需查阅技能包内的原始资料:

  • .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_tagsautoScalingStrategy 等较新的选项),动手前先核对当前版本对应的官方文档,并以仓库内 config/horizon.php 的真实形态为基线——Coolify 这份配置本身就是一份"环境变量参数化 supervisor"的极佳范本,值得在自建 Laravel 应用时对照学习。

登录后查看全文
热门项目推荐
相关项目推荐