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を導入するにあたり、以下の環境要件を満たしている必要があります。
- Redisサーバー:HorizonはRedis専用に設計されているため、
QUEUE_CONNECTION=redisの設定が前提です。 - PHP拡張モジュール(pcntl & posix):プロセスの生成・シグナルハンドリングを行うため、Linux環境で
pcntlおよびposixエクステンションが有効化されている必要があります。 - 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(長時間バッチは個別設定) |
・
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 は、プロセス終了シグナルを受信してから強制終了(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 を使用します。
php artisan horizon:terminateが実行される。- Horizonマスタープロセスは「新規ジョブの受付を停止」し、子ワーカーに対して「現在実行中のジョブが完了したら終了せよ」と指示する。
- すべてのジョブが綺麗に終了した時点で、Horizonプロセスが正常終了(exit)する。
- 常駐監視している 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キューの内部状態が完全に可視化され、障害検知の迅速化と負荷に応じた柔軟なスケーリングが手に入ります。
.envでQUEUE_CONNECTION=redisが正しく設定されているか?HorizonServiceProviderで本番ダッシュボードのアクセス制限(Gate)を設定したか?- Supervisor設定に
stopwaitsecs=3600を含めて長時間ジョブの中断を防いでいるか? - デプロイスクリプトで
php artisan horizon:terminateを実行し、Graceful再起動を行っているか? - Slack Webhook を連携し、ジョブ失敗時に即座にアラートが届くよう設定したか?
キューと非同期処理の全体像や、Jobクラスの基本設計・ディスパッチパターンの基礎を再確認したい場合は、親ピラー記事である Laravel キュー(Queue)と非同期処理の実装完全ガイド も合わせてご活用ください。

コメント