Deployment · Open source
Kiln
Artisan commands and an optional signed HTTP route to clear, warm and inspect OPcache when you deploy.
What it solves
With opcache.validate_timestamps=0, the recommended production setting, a Laravel app keeps serving stale bytecode after a deploy until the process pool restarts.
Resetting OPcache from php artisan does not fix it, because the CLI and the FPM worker pool do not share the same OPcache.
Kiln adds kiln:clear, kiln:warm, kiln:status and kiln:config, plus an optional route that runs the clear or warm inside an FPM worker. The route only accepts temporary signed URLs, and clear and warm are POST-only.
- Four Artisan commands: kiln:clear, kiln:warm, kiln:status and kiln:config
- kiln:warm compiles files with opcache_compile_file(), which does not run their top-level code
- Optional HTTP route that clears or warms OPcache inside an FPM worker
- The route refuses any signed URL without an expiry, even if the signature is valid
- clear and warm are POST-only, so link prefetchers and scanners cannot trigger them
- Warming over HTTP stops after 20 seconds by default and reports partial progress
Install
Requires PHP 8.3+ and Laravel 12 and 13.
composer require amjadiqbal/kiln
Usage
// .env: KILN_ROUTE_ENABLED=true
use Illuminate\Support\Facades\URL;
$url = URL::temporarySignedRoute('kiln.clear', now()->addMinutes(5));
// Then, from your deploy script: curl -fsS -X POST "$url"
Why Kiln exists
appstract/laravel-opcache did this job for years, but its last release, 4.0.2, shipped in December 2020. It requires guzzlehttp/guzzle: ^6.3.1|^7.0, and a fresh Laravel 13 app ships Guzzle 8, so 4.0.2 cannot be installed there. A plain composer require appstract/laravel-opcache on Laravel 13 does not fail, though: Composer quietly falls back to 2.0.5 (December 2019) plus appstract/lush-http, which Packagist lists as abandoned. On Laravel 12 (Guzzle 7) 4.0.2 still installs. Kiln is a from-scratch replacement for PHP 8.3+ and Laravel 12/13. It sets no Guzzle constraint of its own.
Setup
Package auto-discovery registers the service provider. Publish the config to change the warmed paths or enable the HTTP route.
- 1 composer require amjadiqbal/kiln
- 2 php artisan vendor:publish --tag=kiln-config
Artisan commands
A php artisan process has its own OPcache, separate from the FPM worker pool's. Run from the CLI, kiln:clear and kiln:warm only affect the CLI's OPcache and print a warning saying so. --time-limit bounds a warm run; on the CLI there is no limit by default.
php artisan kiln:clear # opcache_reset() in this process
php artisan kiln:warm # compile every file under config('kiln.warm_paths') into OPcache
php artisan kiln:status # hit rate, memory usage, cached script count
php artisan kiln:config # dump the relevant opcache.* ini directives, flag risky settings
php artisan kiln:warm --time-limit=15
The HTTP route for FPM deploys
Set KILN_ROUTE_ENABLED=true to register POST /kiln/opcache/clear, POST /kiln/opcache/warm and GET /kiln/opcache/status. Every route needs a temporary signed URL; a URL made with URL::signedRoute() has no expiry and is refused. Clearing once resets the shared OPcache for every worker in that pool. warm is bounded by kiln.warm_time_limit (20 seconds by default) and reports "timed_out": true with partial progress when it hits the limit.
url=$(php artisan tinker --execute="echo URL::temporarySignedRoute('kiln.clear', now()->addMinutes(5));")
curl -fsS -X POST "$url"
Behind a reverse proxy
If every call returns 403 Invalid signature although the URL and APP_KEY are correct, the app is probably not seeing the public scheme and host, usually because TrustProxies is not set up. Fix TrustProxies first. Otherwise set KILN_ABSOLUTE_SIGNATURE=false and generate every URL with absolute: false; the flag must match on both ends.
URL::temporarySignedRoute('kiln.clear', now()->addMinutes(5), [], absolute: false)
Hire me
Need this built into your app, or something custom?
I wrote Kiln. I can wire it into your Laravel app, extend it for your case, or build the feature around it. Tell me what you need and you'll get a plan and a quote, free.
Support
Found a bug or want a feature? Open an issue on GitHub. For help adding it to your own project, use the quote form above.
More Laravel packages
All packages
Laravel Driver.js
Build Driver.js product tours in PHP and render them with one Blade component.
Laravel Sonner
Sonner toast notifications for Laravel, sent from PHP and shown without React.
Laravel Ninja Keys
A Cmd+K / Ctrl+K command palette for Laravel, with the actions defined in PHP.
Laravel SweetAlert2
SweetAlert2 popups, toasts and confirm dialogs from Laravel controllers, Livewire and Alpine.
Laravel Grid.js
Turn any Eloquent query into a searchable, sortable, paginated Grid.js data table with one fluent PHP call.
Laralink
Work on a Composer package inside the Laravel app that uses it, then switch back to the Packagist release.
Refresher
A drop-in replacement for Laravel's RefreshDatabase that restores a cached snapshot of your test database instead of running migrate:fresh.
Laravel Media Viewer
Show private PDFs, video and audio from any Laravel disk without making the files public.
Laravel Swiper
Swiper sliders, carousels and image galleries for Laravel as Blade components.
Laravel MySQL Advisor
Read-only health checks for the MySQL or MariaDB database behind your Laravel app, with the SQL and the Laravel migration to fix a problem wherever a fix applies.
Laravel Webhook Console
A dashboard and replay tool for the webhooks spatie/laravel-webhook-client stores in your Laravel app.