Back to Blog
Tutorial12 min read2026-07-02

How to Deploy Laravel Octane for High Performance

Laravel Octane keeps your app booted in memory for dramatically higher throughput. This guide covers choosing FrankenPHP vs Swoole, containerizing Octane, and the stateful-app pitfalls that bite teams in production.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

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.

RuntimeLanguageStrengthsConsiderations
FrankenPHPGo (Caddy-based)Built-in HTTPS, HTTP/2/3, easy worker mode, official Docker imageNewer; fast-moving
SwooleC extensionMature, concurrency primitives, task workersRequires PECL extension; more moving parts
RoadRunnerGoStable, good observabilitySeparate 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=frankenphp

Locally 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.js

Compare 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.

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also