Laravel Horizon完全ガイド|Redisキュー監視・ダッシュボード設定・Supervisor連携と通知設計

Laravel入門基本文法・構文ガイド実装・応用テクニック運用・保守・セキュリティ

LaravelでWebアプリケーションを運用する際、メール送信や画像変換、外部API通信などの重い処理をバックグラウンドへ逃がす「キュー(Queue)と非同期処理」は欠かせない基盤アーキテクチャです。

しかし、標準の php artisan queue:work コマンドだけで運用していると、「現在どれくらいのジョブが処理されているのか」「処理待ちキューが滞留していないか」「どのジョブがなぜ失敗したのか」といった稼働状況をブラックボックスのまま手探りで監視しなければなりません。

この運用の不透明さを解消し、Redisキューの圧倒的な可視化とオートスケーリングを実現する公式パッケージが Laravel Horizon(ホライズン) です。

📌 本記事でマスターできること:

  • Laravel Horizonの基本機能と通常のqueue:workとの決定的な違い
  • Horizonの導入手順(Composerインストール・アセット公開・マイグレーション)
  • config/horizon.php の詳細設定(プロセスのオートバランシングと環境別分離)
  • 本番環境での安全なセキュリティ設計(Gateによる認可とBasic認証)
  • Supervisorによるデーモン常駐化とプロセス自動復旧設定
  • デプロイ時のGraceful再起動(horizon:terminate)とCI/CD連携
  • Slack通知によるジョブ異常検知とタグによる個別ジョブ追跡
  • キューが流れない・停止する場合のトラブルシューティング

Laravel Horizonとは?導入メリットと前提要件

Laravel Horizonは、Laravel専用の美しいWebダッシュボードとコード駆動型設定を提供する公式のRedisキュー管理システムです。

Laravelにおけるキュー処理の基礎やJobクラスのディスパッチ手法については、親ピラー記事である Laravel キュー(Queue)と非同期処理の実装完全ガイド で詳しく解説していますが、HorizonはそのRedisキュー運用を劇的に進化させます。

通常の queue:work と Horizon の違い

標準の queue:work と Laravel Horizon の主な違いを整理しました。

比較項目 標準の queue:work Laravel Horizon
監視ダッシュボード なし(ターミナルログやDBテーブルを直接確認) リアルタイムWeb UIを提供(スループット、処理時間、キュー滞留数をグラフ化)
ワーカープロセスの調整 Supervisorで固定プロセス数を手動設定 自動バランシング(balance: auto)により負荷に応じてプロセス数を自律増減
失敗ジョブの再試行 CLIから php artisan queue:retry を実行 ダッシュボード画面からワンクリックで再試行・スタックトレース閲覧が可能
ジョブの検索・追跡 困難(ログファイルからgrep検索) タグ(Tags)機能により特定ユーザーや注文IDに関連するジョブを即座に特定
対応ドライバ database, redis, sqs, beanstalkd 等 Redis専用(高速インメモリキューに特化)

前提要件と動作環境

Laravel Horizonを導入するにあたり、以下の環境要件を満たしている必要があります。

  1. Redisサーバー:HorizonはRedis専用に設計されているため、QUEUE_CONNECTION=redis の設定が前提です。
  2. PHP拡張モジュール(pcntl & posix):プロセスの生成・シグナルハンドリングを行うため、Linux環境で pcntl および posix エクステンションが有効化されている必要があります。
  3. OS環境の制約:Windowsローカル環境では pcntl 拡張がサポートされていないため、ローカル開発には Docker(Laravel Sail) または WSL2 の利用が強く推奨されます。

Laravel Horizonのインストールと基本セットアップ

Horizonのインストールからブラウザでのダッシュボード表示確認までの手順をステップ形式で解説します。

パッケージのインストールとアセット公開

まずはComposerを使用してパッケージを追加し、設定ファイルとフロントエンドアセットをプロジェクトへ公開します。

# パッケージのインストール
composer require laravel/horizon

# 設定ファイル(config/horizon.php)とアセットの公開
php artisan horizon:install

# マイグレーションの実行
php artisan migrate

horizon:install コマンドを実行すると、以下の成果物が作成されます。

  • config/horizon.php:Horizonのマスター設定ファイル
  • app/Providers/HorizonServiceProvider.php:ダッシュボードへのアクセス権限(Gate)を管理するサービスプロバイダ
  • public/vendor/horizon/:Webダッシュボードを描画するためのCSS/JSアセット

ローカル環境での起動確認

環境変数 .env でキュー接続先がRedisになっていることを確認します。

QUEUE_CONNECTION=redis
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379

ターミナルでHorizonのメインプロセスを起動します。

php artisan horizon

コンソールに Horizon started successfully. と表示されたら、ブラウザから以下のURLにアクセスしてください。

http://localhost:8000/horizon

ダッシュボードが表示され、ステータスが「Active」となっていれば基本セットアップは完了です。


config/horizon.php 設定ファイルの完全解説

Horizonの動作はすべて config/horizon.php で一元管理されます。本番環境で安定したスループットを発揮させるための重要パラメータを解説します。

主要設定パラメータ早見表

設定項目 説明 推奨値 / 例
balance プロセスの配分方式(simple, auto, false) 本番では auto または simple
minProcesses / maxProcesses 自動バランシング時の最小・最大プロセス数 サーバーのCPU・メモリに応じて設定(例: 1〜10)
memory_limit ワーカープロセスが消費できる最大メモリ(MB) 64 または 128
tries ジョブ失敗時の自動リトライ回数 3
timeout ジョブの最大実行秒数(超過時は強制終了) 60(長時間バッチは個別設定)
💡 balance(バランシング方式)の選択基準:
・auto:各キューの待ち時間(Wait Time)に応じて、プロセスを自動的に再配分します。突発的な大量ジョブが発生しやすい環境に最適です。
・simple:各キューに均等にプロセスを割り振ります。予測しやすい定常的な処理に向いています。
・false:プロセスを動的に配分せず、設定値のまま固定で起動します。

環境別設定(environments: production / local)の記述例

本番環境(production)とローカル開発環境(local)でワーカーのスペックやプロセス配分を分離する設定例です。

'environments' => [
    'production' => [
        'supervisor-1' => [
            'maxProcesses' => 10,
            'balanceMaxShift' => 1,
            'balanceCooldown' => 3,
            'connection' => 'redis',
            'queue' => ['high', 'default', 'low'],
            'balance' => 'auto',
            'autoScalingStrategy' => 'time',
            'minProcesses' => 2,
            'memory' => 128,
            'tries' => 3,
            'timeout' => 90,
            'nice' => 0,
        ],
    ],

    'local' => [
        'supervisor-1' => [
            'maxProcesses' => 3,
            'connection' => 'redis',
            'queue' => ['default'],
            'balance' => 'simple',
            'minProcesses' => 1,
            'memory' => 64,
            'tries' => 1,
            'timeout' => 60,
        ],
    ],
],

ダッシュボードのセキュリティ設定(Gate認可とアクセス制御)

Horizonダッシュボードにはジョブの内容や失敗時の例外トレース、ペイロードデータなど、機密情報が多数含まれます。本番環境で一般ユーザーに公開されないよう厳格なアクセス制御を施します。

本番環境でのアクセス制限(HorizonServiceProvider)

デフォルトでは、app()->isLocal()(ローカル環境)でのみアクセスが許可されています。本番環境で管理者アカウントのみに閲覧を許可するには、app/Providers/HorizonServiceProvider.php の gate() メソッドをカスタマイズします。

<?php

namespace App\Providers;

use App\Models\User;
use Illuminate\Support\Facades\Gate;
use Laravel\Horizon\HorizonApplicationServiceProvider;

class HorizonServiceProvider extends HorizonApplicationServiceProvider
{
    /**
     * Horizonダッシュボードへのアクセス権限を判定するGateを登録
     */
    protected function gate(): void
    {
        Gate::define('viewHorizon', function (?User $user) {
            if (! $user) {
                return false;
            }

            // 指定された管理者メールアドレスまたは管理者フラグを判定
            return in_array($user->email, config('horizon.admins', [
                'admin@example.com',
            ])) || $user->is_admin;
        });
    }
}

未ログインユーザーが /horizon にアクセスした場合、自動的に403 Forbiddenまたはログイン画面へとリダイレクトされます。

Basic認証によるダッシュボード保護(必要に応じた代替手段)

Webアプリケーションの認証機能とは独立して、インフラレベルやWebサーバー(Nginx / Apache)レベルで保護したい場合は、Basic認証を組み合わせるのが実務上有効です。

# Nginxでの設定例
location /horizon {
    auth_basic "Restricted Horizon Dashboard";
    auth_basic_user_file /etc/nginx/.htpasswd;
    try_files $uri $uri/ /index.php?$query_string;
}

本番運用の必須設定:Supervisorによるデーモン常駐化

本番サーバーでは、Horizonプロセスがサーバー再起動時や不慮のエラー停止時にも自動で復旧するよう、Linuxプロセスマネージャーである Supervisor でデーモン常駐化します。

Supervisor設定ファイル(/etc/supervisor/conf.d/horizon.conf)の作成

サーバーの /etc/supervisor/conf.d/horizon.conf に以下の設定を記述します。

[program:horizon]
process_name=%(program_name)s
command=php /var/www/html/artisan horizon
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/www/html/storage/logs/horizon.log
stopwaitsecs=3600
⚠️ stopwaitsecs=3600 設定の極めて重要な意味:
stopwaitsecs は、プロセス終了シグナルを受信してから強制終了(SIGKILL)するまでの猶予秒数を指定します。デフォルトの10秒のままだと、数分以上かかる長時間のバッチジョブの実行中にデプロイやサービス停止が行われた際、ジョブが途中で強制切断されてデータの不整合が発生します。そのため、十分な長さ(1時間 = 3600秒)を確保することが本番運用の鉄則です。

Supervisorの再読み込みとステータス確認コマンド

設定ファイルを作成・更新したら、Supervisorに変更を反映させてプロセスを起動します。

# 設定ファイルの構文確認と読み込み
sudo supervisorctl reread

# プロセス定義の更新と自動起動
sudo supervisorctl update

# Horizonプロセスの明示的な起動
sudo supervisorctl start horizon

# 稼働ステータスの確認
sudo supervisorctl status

ステータスが horizon RUNNING pid 12345, uptime 0:01:23 のように表示されれば正常にデーモン常駐化されています。


デプロイ時の運用プラクティスとGraceful再起動

アプリケーションの新バージョンをデプロイする際、キューワーカーへ新しいソースコードを安全に反映させる必要があります。

php artisan horizon:terminate による安全な再起動

デプロイ時に注意すべき操作が sudo supervisorctl restart horizon を直接実行すること です。これを実行すると、現在ワーカーが実行中のジョブに対して即座に停止シグナルが送られ、処理が中途半端に中断される危険があります。

安全に再起動を行うためには、Laravelが提供する php artisan horizon:terminate を使用します。

🔄 Gracefulシャットダウンのメカニズム:

  1. php artisan horizon:terminate が実行される。
  2. Horizonマスタープロセスは「新規ジョブの受付を停止」し、子ワーカーに対して「現在実行中のジョブが完了したら終了せよ」と指示する。
  3. すべてのジョブが綺麗に終了した時点で、Horizonプロセスが正常終了(exit)する。
  4. 常駐監視している Supervisorが「プロセスが終了した」ことを検知し、最新のソースコードを読み込んで新規プロセスを自動再起動 する。

CI/CD環境(GitHub Actions / Deployer)への組み込み例

デプロイスクリプト(GitHub ActionsやDeployer)の末尾に、キャッシュクリアと horizon:terminate を組み込むのが基本です。

# デプロイスクリプトの例
git pull origin main
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache

# Horizonを安全にGraceful再起動
php artisan horizon:terminate

ジョブ失敗時の通知設計と監視機能

Horizonはジョブの実行監視だけでなく、ジョブが失敗した瞬間にチームへアラートを発報する通知機能を標準で備えています。

Slack Webhook を利用したジョブ失敗通知

App\Providers\HorizonServiceProvider 内で通知のルーティングを設定することで、Slackやメールに即座に通知できます。

<?php

namespace App\Providers;

use Laravel\Horizon\Horizon;
use Laravel\Horizon\HorizonApplicationServiceProvider;

class HorizonServiceProvider extends HorizonApplicationServiceProvider
{
    public function boot(): void
    {
        parent::boot();

        // 障害発生時にメール通知
        Horizon::routeMailNotificationsTo('dev-alerts@example.com');

        // SlackのWebhook URLへ通知
        Horizon::routeSlackNotificationsTo(
            config('services.slack.webhook_url'),
            '#alerts-queue'
        );
    }
}

さらに、config/horizon.php 内の waits 設定を定義しておけば、特定のキューでジョブの待ち時間が閾値(例: 60秒)を超えた際に「キュー滞留警告」のアラートを上げることも可能です。

'waits' => [
    'redis:default' => 60,
    'redis:high' => 30,
],

タグ付け機能(Tags)による特定モデル・ユーザーのジョブ追跡

Horizonは、Jobクラスのプロパティに含まれるEloquentモデルを自動的に検知し、ダッシュボード上でタグ(App\Models\User:123 など)としてインデックス化します。

カスタムタグを手動で付与したい場合は、Jobクラス内に tags() メソッドを定義します。

<?php

namespace App\Jobs;

use App\Models\Order;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class ProcessOrderJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(
        public Order $order
    ) {}

    public function handle(): void
    {
        // 注文処理
    }

    /**
     * Horizonダッシュボードで検索するためのカスタムタグ
     */
    public function tags(): array
    {
        return [
            'order:' . $this->order->id,
            'user:' . $this->order->user_id,
            'tier:' . $this->order->plan_tier,
        ];
    }
}

これにより、ダッシュボード上部の検索ボックスに order:4567 と入力するだけで、該当する注文処理ジョブのステータスや実行時間を一瞬で絞り込むことができます。


トラブルシューティング(キューが流れない・停止する場合の対処法)

Horizon運用時に発生しやすいトラブルとその原因・解決手順をまとめました。

よくあるトラブルと解決策一覧

トラブル事象 主な原因 解決策
ダッシュボードのステータスが「Inactive」のまま Horizonプロセスが起動していない、またはRedisサーバーと通信できていない。 php artisan horizon を起動するか、Supervisorのステータス(supervisorctl status)およびRedis接続(redis-cli ping)を確認してください。
ジョブがキューに積まれるが処理されない config/horizon.php の監視対象キュー名とディスパッチ時のキュー名が不一致。 ジョブの onQueue('name') で指定したキュー名が、Horizon設定の queue 配列(['high', 'default'] 等)に含まれているか確認してください。
Allowed memory size exhausted エラーでプロセスが死ぬ 大量データの一括ロードによりPHPのメモリ上限に到達している。 config/horizon.php の memory 上限を引き上げるか、ジョブ内部のクエリを chunkById() や cursor() で小分け処理に改修してください。
本番環境で /horizon にアクセスすると403 Forbiddenになる HorizonServiceProvider の viewHorizon Gateでアクセスが拒否されている。 ログイン中ユーザーのメールアドレスが config/horizon.admins に登録されているか、Gateの認可条件を見直してください。

まとめ:Laravel Horizonで堅牢な非同期処理基盤を構築しよう

Laravel Horizonを導入することで、これまでCLIログの奥深くに隠れていたRedisキューの内部状態が完全に可視化され、障害検知の迅速化と負荷に応じた柔軟なスケーリングが手に入ります。

✅ Horizon本番運用のチェックリスト:

  • .env で QUEUE_CONNECTION=redis が正しく設定されているか?
  • HorizonServiceProvider で本番ダッシュボードのアクセス制限(Gate)を設定したか?
  • Supervisor設定に stopwaitsecs=3600 を含めて長時間ジョブの中断を防いでいるか?
  • デプロイスクリプトで php artisan horizon:terminate を実行し、Graceful再起動を行っているか?
  • Slack Webhook を連携し、ジョブ失敗時に即座にアラートが届くよう設定したか?

キューと非同期処理の全体像や、Jobクラスの基本設計・ディスパッチパターンの基礎を再確認したい場合は、親ピラー記事である Laravel キュー(Queue)と非同期処理の実装完全ガイド も合わせてご活用ください。

レン (Wren)

こんにちは。レンです。

Laravelのコードの森に住んでいる、小さな案内役です。
ルーティングの枝やクラスの影を歩きながら、コードの流れや仕組みを眺めています。

このサイトでは、Laravelの基本から実装のコツまで、開発で役立つポイントを静かに整理しています。
難しいことを増やすのではなく、コードの見通しが少し良くなるヒントを届けるのが役目です。

「この処理はどこに書くのがいいのか」
「Laravelではどう考えると整理できるのか」

そんな疑問に、小さなメモを残すような気持ちで記事を書いています。

コードを書いている途中で迷ったとき、
このサイトが少し立ち止まって整理できる場所になればうれしいです。

レン (Wren)をフォローする

コメント