Migrating from Laravel Horizon
Swap the Composer package, redeploy, restart the workers. No code changes, and no one-way door.
Migrating from Laravel Horizon to Laravel Skyline needs no application code changes and no migrations.
Skyline keeps the Laravel\Horizon\ namespace and declares replace: laravel/horizon in
Composer, so every import, gate, config key and Artisan command you already have keeps resolving — against Skyline
instead of Horizon. In practice the migration is a Composer swap, a deploy, and a worker restart.
The migration#
- Remove
laravel/horizonfrom therequireblock ofcomposer.json. - Add the Anystack registry and require Skyline — see Installation for the license-key setup.
- Run
composer update. - Deploy, then restart the workers with
php artisan horizon:terminate.
composer remove laravel/horizon
composer config repositories.skyline composer https://laravel-skyline.composer.sh
composer require boring-o11y/laravel-skyline
php artisan horizon:terminate
Composer will refuse to install both packages at once. That is the intended behaviour rather than a conflict to work
around — replace is what makes every third-party package that depends on laravel/horizon
resolve cleanly against Skyline.
What keeps working, untouched#
- Your
config/horizon.php, copied across unchanged. Every Skyline key has a working default. - Your
HorizonServiceProvider, includingGate::define('viewHorizon', …)andHorizon::auth(). - Every
use Laravel\Horizon\…import in your application code. - Every
php artisan horizon:*command, by name and behaviour. - The dashboard, still mounted at
/horizon. - The jobs already sitting in Redis: the payload format does not change, so nothing needs to drain first.
Skyline ships its own compiled dashboard assets. If your deploy publishes Horizon's assets into
public/, add php artisan horizon:publish to it — otherwise you will be serving
Horizon's old JavaScript against Skyline's API and wondering why the new tabs are missing.
Deploying without dropping jobs#
The swap is an ordinary deploy, and the ordinary deploy rules apply. Workers run the code they booted with, so a
worker started before the deploy keeps running Horizon's code until it is restarted — which is exactly what
horizon:terminate is for: it lets each worker finish the job in its hands and then exit, and your process
supervisor restarts it on the new code.
There is no window in which jobs are lost. A job dispatched by new code and picked up by a not-yet-restarted worker is an ordinary Redis payload either way; the two versions read and write the same format.
Verifying the migration#
After the workers come back, three things tell you the swap took:
composer show boring-o11y/laravel-skylinereports the installed version.- The dashboard at
/horizonshows the tabbed Jobs view, with Scheduled and Retries as separate tabs. - Opening a queue from the dashboard drills into the jobs waiting inside it, rather than only counting them.
If the dashboard looks exactly as it did before, you are almost certainly serving stale published assets — see the note above.
Rolling back#
Rolling back is the same operation in reverse, and it is deliberately cheap:
composer remove boring-o11y/laravel-skyline
composer require laravel/horizon
php artisan horizon:publish
php artisan horizon:terminate
Skyline adds no database schema and stores its extra tracking data under the same Redis prefix Horizon already uses. A rollback therefore loses the Skyline-only views — the trend charts, the delayed-job index, the previous-attempt panel — and leaves your queues, your jobs and your metrics history untouched. The Skyline keys left behind in Redis are trimmed on their own retention schedule and cost nothing.
A queueWeights map or a worker_output key left in config/horizon.php after
a rollback is simply ignored by upstream Horizon, which reads only the keys it knows about. You do not have to
strip them out to go back.
If the subscription lapses#
Already-installed versions keep running normally — there is no runtime license check that can interrupt job
processing. Only future composer update calls against the private registry fail authentication, until
the subscription is renewed. A lapsed license is a build-time problem, never a production one.