Traditional Laravel boots the entire framework on every request: load the container, register service providers, parse config. Under PHP-FPM that's fine for moderate traffic but wasteful at scale. Laravel Octane keeps the application booted in memory and serves requests from long-lived workers, eliminating per-request bootstrap. The result can be a several-x throughput improvement — but Octane changes the execution model in ways that break naive code. This guide covers deploying it correctly.
How Octane changes the game
Under FPM, each request gets a fresh process. Memory leaks don't matter much because the process dies. Global state resets automatically. Octane breaks both assumptions: workers persist across requests, so anything you stash in a static property, a singleton, or a container binding survives into the next request — including, potentially, another user's data.
This is the single most important thing to understand before deploying Octane: your code must be stateless between requests. The performance is real, but you're trading the safety of process-per-request for speed.
Choosing a runtime: FrankenPHP vs Swoole vs RoadRunner
Octane supports three application servers. As of Laravel 11+, FrankenPHP is the recommended default.
| Runtime | Language | Strengths | Considerations |
|---|---|---|---|
| FrankenPHP | Go (Caddy-based) | Built-in HTTPS, HTTP/2/3, easy worker mode, official Docker image | Newer; fast-moving |
| Swoole | C extension | Mature, concurrency primitives, task workers | Requires PECL extension; more moving parts |
| RoadRunner | Go | Stable, good observability | Separate binary to manage |
For most teams deploying to containers, FrankenPHP is the path of least resistance because it ships as a single binary with an official base image and handles the web server too. The examples below use FrankenPHP.
Installing Octane
composer require laravel/octane
php artisan octane:install --server=frankenphpLocally you can run:
php artisan octane:frankenphp --workers=4 --max-requests=512--max-requests is your safety net: workers restart after N requests, which contains slow memory leaks. Set it conservatively at first (256–512) and raise it once you've confirmed your app is leak-free.
A production Dockerfile
FrankenPHP provides a base image that already bundles PHP and the server:
FROM dunglas/frankenphp:1-php8.3 AS base
WORKDIR /app
# System deps for common Laravel extensions
RUN install-php-extensions pdo_pgsql pdo_mysql redis intl zip opcache
COPY composer.json composer.lock ./
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer \
&& composer install --no-dev --no-scripts --optimize-autoloader --no-interaction
COPY . .
RUN composer dump-autoload --optimize \
&& php artisan config:cache \
&& php artisan route:cache \
&& php artisan view:cache
ENV OCTANE_SERVER=frankenphp
EXPOSE 8000
CMD ["php", "artisan", "octane:frankenphp", "--host=0.0.0.0", "--port=8000", "--max-requests=512"]Note the config:cache and friends — caching config and routes is doubly important under Octane because you want everything resolved at boot, not per request. Enable OPcache (and consider OPcache JIT) for an additional boost.
Deploying the container
Push the repo to a platform that builds from a Dockerfile. On PandaStack, connecting the Git repo triggers a rootless BuildKit build in an ephemeral Kubernetes Job, the image lands in the registry, and Helm rolls it out. Set the listening port to match (8000 above) and point the health check at a lightweight route.
Add a simple health route that doesn't touch the database for liveness, and a separate readiness route that does:
Route::get('/up', fn () => response('ok')); // liveness
Route::get('/ready', fn () => DB::connection()->getPdo() ? response('ok') : response('', 503));Database and cache wiring
Laravel reads DATABASE_URL (or the discrete DB_* vars). On PandaStack, attaching a managed PostgreSQL or MySQL auto-injects DATABASE_URL, and Laravel's database config can consume it directly. Managed MySQL (5.7/8.x) and PostgreSQL (14.x/16.x) are both available, so use whichever your app targets.
Under Octane, watch your connection count. Each worker holds its own DB connection. If you run 8 workers across 3 replicas, that's 24 persistent connections — and free-tier managed databases cap connections (50 on the free tier). Size workers and replicas against your DB's connection limit, or put a pooler in front.
For sessions, cache, and queues, use Redis rather than the file or array drivers. A managed Redis instance keeps state out of the worker memory where it belongs.
The stateful-app pitfalls (read this twice)
Octane's docs list these, and ignoring them causes the scariest class of bugs — cross-request data leakage:
- Singletons holding request state. A service provider that binds a singleton referencing the current request or user will leak that reference into later requests. Bind such services as scoped or resolve fresh per request.
- Static properties accumulating data. Static arrays that you append to will grow unbounded across requests — a memory leak and a correctness bug.
- The container itself. Use
Octane::concurrently()and the provided helpers; reset any state you mutate. - Global PHP state.
setlocale,date_default_timezone_set, and similar global calls persist. Set them per request if they vary.
Laravel ships listeners (RequestReceived, RequestTerminated) and the octane:reload workflow to help. Test under load before trusting it: hammer the app with a tool like k6 or wrk and assert that responses for different users never bleed together.
Measuring the win
Don't take throughput claims on faith — benchmark your app, not a hello-world. A representative test:
k6 run --vus 50 --duration 30s load-test.jsCompare requests/sec and p95 latency between an FPM build and an Octane build of the same app. The gain is real but workload-dependent: CPU-bound apps with heavy bootstrap benefit most; apps dominated by slow downstream calls benefit least.
When not to use Octane
If your traffic is modest and your team isn't ready to audit for stateful bugs, plain Laravel on FPM is safer and perfectly fast. Octane pays off when you have meaningful traffic, a CPU-bound bootstrap, and the discipline to keep request handling stateless.
References
- Laravel Octane docs: https://laravel.com/docs/octane
- FrankenPHP: https://frankenphp.dev/docs/
- Swoole: https://www.swoole.com/
- RoadRunner: https://roadrunner.dev/docs
- k6 load testing: https://k6.io/docs/
---
Want to ship Octane without managing the database or build pipeline? PandaStack builds your Dockerfile, auto-wires managed Postgres/MySQL via DATABASE_URL, and includes a free tier to start. Deploy at https://dashboard.pandastack.io.