amjadiqbal/kiln

Deploy-time OPcache control for Laravel: clear, warm and inspect OPcache safely, including across FPM workers via a signed HTTP route.
4
Install
composer require amjadiqbal/kiln
Latest Version:v1.0.0
PHP:^8.3|^8.4|^8.5
License:MIT
Last Updated:Sep 20, 2026
Links: GitHub  ·  Packagist
Maintainer: amjadiqbal

Kiln: Deploy-Time OPcache Control for Laravel

Kiln banner

Artisan commands (and an optional signed HTTP route) to clear, warm, and inspect PHP's OPcache for Laravel apps deployed with opcache.validate_timestamps=0 — the recommended production setting under Octane, FrankenPHP, or any standard FPM deploy. Without invalidating OPcache on every deploy, your app keeps serving stale bytecode until the process pool restarts.

Tests Latest Version Total Downloads License

Features

  • kiln:clear, kiln:warm, kiln:status, kiln:config — four focused artisan commands, no bloat.
  • A signed, POST-only HTTP route for the case artisan commands structurally can't cover: invalidating an FPM worker pool's shared OPcache from a CLI deploy script.
  • Every signed URL is required to be temporary — a dedicated middleware refuses a permanent one even if it's otherwise valid, closing a real leaked-credential risk before it can happen.
  • Zero HTTP client dependency — no Guzzle, unlike the incumbent this replaces.
  • PHP 8.3–8.5, Laravel 12 and 13, CI-verified on every combination before every release.

Why this exists

appstract/laravel-opcache did this job for years (138k+ downloads/month) but has been dead since December 2020 and pins guzzlehttp/guzzle: ^6.3.1|^7.0 — a version ceiling Guzzle has long since moved past, which means it cannot even be installed on a current Laravel app. Kiln is a from-scratch replacement targeting PHP 8.3+ and Laravel 12/13, with zero HTTP client dependency.

The one thing you need to understand before using this

A php artisan process and your FPM worker pool do not share the same OPcache. Each PHP SAPI process group gets its own OPcache shared-memory segment. Running php artisan kiln:clear over SSH clears the CLI's OPcache — useful for confirming your config, but it does not touch the bytecode your FPM workers are actually serving to real requests.

To invalidate OPcache for real traffic, the operation has to run inside a worker that shares memory with the pool serving your site. That's what the HTTP route is for — see below.

Installation

composer require amjadiqbal/kiln

Laravel's package auto-discovery registers the service provider automatically. Publish the config if you want to change the warmed paths or enable the HTTP route:

php artisan vendor:publish --tag=kiln-config

Artisan commands

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

kiln:warm compiles files with opcache_compile_file(), which does not execute the file's top-level code — it's safe to run against app/, config/, and routes/ without triggering side effects, migrations, or route registration twice. Pass --time-limit=<seconds> to bound a run (unlimited by default on the CLI):

php artisan kiln:warm --time-limit=15

Every command warns explicitly when it detects it's running under the CLI SAPI, since that's the process whose OPcache doesn't matter for production traffic.

The HTTP route — for real FPM deploys

Enable it in config/kiln.php (or via .env):

KILN_ROUTE_ENABLED=true

This registers three routes:

  • POST /kiln/opcache/clear
  • POST /kiln/opcache/warm
  • GET /kiln/opcache/status

clear and warm are state-changing, so they're POST-only — a GET request from a link prefetcher, chat-app unfurler, security scanner, or corporate proxy can't trigger them by accident. status is read-only and stays GET.

Every route requires a TEMPORARY signed URL — never a permanent one. Generate one with a short expiry from your deploy script or a scheduled job:

use Illuminate\Support\Facades\URL;

$url = URL::temporarySignedRoute('kiln.clear', now()->addMinutes(5));

Then, from your deploy script:

url=$(php artisan tinker --execute="echo URL::temporarySignedRoute('kiln.clear', now()->addMinutes(5));")
curl -fsS -X POST "$url"

Do not use URL::signedRoute() (no expiry) for this. A signature with no expires parameter is a permanent credential that stays valid until you rotate APP_KEY — exactly the kind of thing that ends up in a deploy log, shell history, or ps output. If it leaks, it's a standing, unauthenticated remote OPcache-reset endpoint, and repeated resets on a busy app force a full bytecode recompilation storm — a genuine DoS, not a theoretical one. Kiln's own RequireTemporarySignature middleware rejects any otherwise-valid signature that has no expires parameter, so a permanent URL is refused even if one gets generated by mistake.

Hitting clear once resets the shared OPcache segment for every worker in that pool — this is standard Zend OPcache shared-memory behaviour, not something Kiln does itself; the route exists purely so the reset happens inside an FPM worker instead of a separate CLI process.

warm is bounded by kiln.warm_time_limit (20 seconds by default) so a large recursive walk can't pin an FPM worker for its entire duration — on a small pool that would be an outage. If the limit is hit, the response reports "timed_out": true with whatever partial progress it made; run it again to continue, or raise the limit.

Troubleshooting: every call gets "403 Invalid signature", but the URL and APP_KEY are correct

This almost always means the app isn't seeing the same public scheme/host that the URL was signed against — typically a reverse proxy (CloudPanel/nginx, Cloudflare, or similar) terminating TLS and forwarding the request internally over plain HTTP without TrustProxies configured to read X-Forwarded-Proto/X-Forwarded-Host. Laravel's own signature check recomputes the full scheme://host from the request it actually receives; if that differs from what the URL was signed against, the HMAC will never match, no matter how correct APP_KEY and the URL are.

Fix TrustProxies first if you can — it fixes every other absolute-URL-dependent thing in Laravel too, not just this. If that's not available to you, set:

KILN_ABSOLUTE_SIGNATURE=false

and generate every URL with absolute: false too — the flag must match on both ends, since it's baked into the signature itself, not just how it's checked:

URL::temporarySignedRoute('kiln.clear', now()->addMinutes(5), [], absolute: false)

Configuration

// config/kiln.php
return [
    'warm_paths' => ['app', 'bootstrap/cache', 'config', 'routes'],
    'warm_vendor' => false,
    'warm_time_limit' => env('KILN_WARM_TIME_LIMIT', 20),
    'route' => [
        'enabled' => env('KILN_ROUTE_ENABLED', false),
        'prefix' => env('KILN_ROUTE_PREFIX', 'kiln'),
        'middleware' => [], // add 'web' yourself if you genuinely want a session on this endpoint
        'absolute_signature' => env('KILN_ABSOLUTE_SIGNATURE', true), // see Troubleshooting above
    ],
];

Testing

composer test       # Pest, via Orchestra Testbench
composer pint        # Laravel Pint, --test mode
composer analyse     # Larastan/PHPStan

Every push and pull request runs the full suite across PHP 8.3/8.4/8.5 × Laravel 12/13 (6 combinations) before anything merges — see the badge above.

Support & Community

Custom Development

Hire me on Upwork for:

  • Package integration
  • Custom feature development
  • Technical consultation
  • Project implementation

Community Support

For priority support and enterprise solutions, please reach out via Upwork for direct assistance.

Changelog

Please see CHANGELOG.md for details on what changed in each release.

Security

If you discover a security vulnerability, please reach out privately via Discord or Upwork instead of opening a public issue. It will be addressed promptly.

Contributing

Contributions, issues, and feature requests are welcome — see open issues or open a pull request.

License

MIT. See LICENSE.md.

Related Packages

maximkou/laravel-opcache-clear

This Laravel package allows you to clear the OPcache of a PHP application runnin...

5,553 1
neo/laravel-opcache

This Laravel package allows you to clear the OPcache of a PHP application runnin...

99 0
devoption/beacon

Production-ready Docker and Helm support for Laravel applications, with guided i...

3 1
jsefton/laravel-remote-deploy

Deploy Laravel sites to a remote server directly from Artisan

49 2