Deploying Laravel with FrankenPHP on a VPS changes the performance profile of your application more than any single infrastructure decision you can make. PHP-FPM has served the framework well for fifteen years, but it boots Laravel from scratch on every request – 50 to 150 milliseconds of framework bootstrap before your first line of business logic runs. FrankenPHP is a modern PHP application server written in Go and built on top of Caddy. It keeps your Laravel application in memory across requests, serving traffic through long-lived worker processes instead of forking a new PHP process for every HTTP request.
Why deploy Laravel with FrankenPHP on a VPS?
One binary, no stack
FrankenPHP ships as a single static binary that includes the PHP runtime, Caddy web server, and all commonly used extensions. There is no nginx or Apache to configure. No PHP-FPM pool to tune. No Certbot cron job for SSL renewal. Caddy handles automatic HTTPS via Let’s Encrypt out of the box. The traditional Laravel production stack requires at least two services (web server + PHP-FPM), a TLS termination layer, and a process supervisor. FrankenPHP collapses that into one binary and one service.
Officially recommended Octane runtime
FrankenPHP is the officially recommended Octane backend in the Laravel documentation. The Laravel team ships first-party install commands and has published benchmark results showing FrankenPHP handling significantly higher throughput than PHP-FPM on the same hardware. Previous Octane drivers – Swoole and RoadRunner – required separate extensions or binaries beyond the PHP ecosystem. Swoole needs a PECL extension compiled into PHP. RoadRunner needs a Go binary deployed alongside your application. FrankenPHP runs PHP code directly via an embedded SAPI with no additional dependencies.
HTTP/2, HTTP/3, and early hints
Caddy provides HTTP/2 and HTTP/3 support without additional configuration. Early hints (103 status code) let the server push critical assets to the browser before the main response is ready. These features are available automatically when you deploy Laravel with FrankenPHP on a VPS – no module compilation or reverse proxy setup required. The protocol support alone eliminates a separate nginx layer for most deployments.
Prerequisites for deploying Laravel with FrankenPHP
Before you start, make sure your VPS meets these requirements:
- Ubuntu 24.04 (or any modern Linux distribution with systemd)
- PHP 8.2 or later installed with extensions: curl, mbstring, pdo, pdo_pgsql or pdo_mysql, redis, sodium, openssl
- Composer installed globally
- Git
- A domain name pointed at your VPS IP address
- SSH access configured with key-based authentication
If you manage credentials across multiple workstations, the guide on managing SSH credentials across multiple devices covers keeping keys and configuration synchronized across machines. Having your SSH access organized before you start saves time during the deploy setup and prevents credential sprawl as you add more servers.
How to install FrankenPHP on your VPS
FrankenPHP distributes prebuilt static binaries for Linux on the GitHub releases page. The install takes two commands and completes in under a minute.
curl -L https://github.com/dunglas/frankenphp/releases/latest/download/frankenphp-linux-x86_64 -o /tmp/frankenphp
chmod +x /tmp/frankenphp
sudo mv /tmp/frankenphp /usr/local/bin/frankenphp
Verify the install:
frankenphp version
# Example output: FrankenPHP 1.x (built from sha: abc123)
That is the whole install. One binary, no apt repositories, no package manager dependencies. The file is about 40 MB and includes PHP 8.4, all common extensions (curl, mbstring, pdo, pgsql, redis, sodium, openssl), and the Caddy web server. If you are on an ARM-based VPS (Graviton, Ampere), replace x86_64 with aarch64 in the download URL.
How to set up Laravel Octane with FrankenPHP
Laravel Octane is the package that bridges Laravel with high-performance application servers. Install it with Composer and choose the FrankenPHP driver. Octane handles the worker lifecycle, graceful reload, and request dispatching.
composer require laravel/octane
php artisan octane:install --server=frankenphp
The install command downloads the FrankenPHP binary into vendor/bin and writes config/octane.php. Open that file and tune two settings: worker count and max requests.
return [
'server' => env('OCTANE_SERVER', 'frankenphp'),
'workers' => env('OCTANE_WORKERS', 4),
'task_workers' => env('OCTANE_TASK_WORKERS', 6),
'max_requests' => env('OCTANE_MAX_REQUESTS', 500),
];
Workers default to one per CPU core, which is sensible for CPU-bound applications. For IO-heavy applications, you can bump slightly above core count. Task workers handle queued operations that benefit from concurrent execution, such as HTTP client requests or encryption operations. Max requests is your safety net against memory creep. Each worker is recycled after serving that many requests, releasing any leaked memory. Do not skip this – every long-running PHP application accumulates memory over time, and max_requests is the automatic cleanup mechanism that prevents unbounded growth.
How to run FrankenPHP as a systemd service
Running FrankenPHP directly from the command line works for testing. For production, you need a process supervisor that starts the server on boot and restarts it if it crashes. systemd is already on your Ubuntu VPS and handles this better than installing Supervisor.
[Unit]
Description=FrankenPHP Octane Server
After=network.target
[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/var/www/your-app
ExecStart=/usr/bin/php /var/www/your-app/artisan octane:frankenphp --host=127.0.0.1 --port=8000 --workers=4 --max-requests=500
ExecReload=/usr/bin/php /var/www/your-app/artisan octane:reload
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
Enable and start the service:
sudo systemctl daemon-reload
sudo systemctl enable octane
sudo systemctl start octane
sudo systemctl status octane
Logs are available through journalctl:
sudo journalctl -u octane -f
The ExecReload directive is what makes zero-downtime deploys possible. Running sudo systemctl reload octane triggers octane:reload, which waits for each worker to finish its in-flight request before swapping in freshly booted code. No requests are dropped during the transition. This is the deploy hook you will wire into your CI/CD pipeline.
How to configure the Caddyfile for production
FrankenPHP uses Caddy under the hood, and Caddy is configured through a Caddyfile. Place this file in your Laravel project root. FrankenPHP reads it automatically on startup.
your-domain.com {
root * /var/www/your-app/public
encode zstd br gzip
php_server {
try_files {path} index.php
}
}
That is the entire configuration. The php_server directive tells Caddy to route PHP requests to the embedded FrankenPHP SAPI. Static assets in public/ are served directly from disk without involving PHP at all. Caddy watches this file and applies changes automatically – no restart required for config changes.
For automatic HTTPS, ensure your domain DNS record points to the VPS IP and the domain in the Caddyfile matches exactly. Caddy queries Let’s Encrypt on first request, obtains a certificate, and renews it automatically. There is no certbot, no webroot challenge configuration, and no cron job for renewal. Certificates are stored in /data/caddy/. If you need to serve multiple domains from the same server, list them on the first line with a space separator.
The deployment workflow
The deploy sequence for a Laravel application running under FrankenPHP follows a specific order. Getting it wrong can serve traffic from old workers against a new database schema or serve stale cached configuration. This is the production sequence that avoids both problems:
cd /var/www/your-app
git pull origin main
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
sudo systemctl reload octane
The order matters for two reasons. First, migrations must run before the reload so new workers boot against the current database schema. If you ran the reload first, the old workers would serve traffic against the old schema while migrations ran. Second, config:cache must run before the reload so workers do not re-read .env mid-recycle, which can produce inconsistent state if environment variables are being updated.
Running the reload last ensures the entire deploy is atomic from the perspective of incoming requests. Old workers handle requests during the deployment steps. New workers are spawned only after the code and configuration are ready. This deploy workflow applies to any setup where you run processes on a remote VPS. If you are automating deploys through CI/CD, the guide on running AI coding agents on remote servers via SSH covers the SSH configuration, connection keepalive tuning, and security considerations that apply to any server-side automation workflow.
Performance: what to expect
Real-world benchmarks from production Laravel applications show consistent improvements when moving from PHP-FPM to Octane with FrankenPHP. On the same hardware – a 4 vCPU, 8 GB RAM VPS – a typical Laravel application with authentication, eager-loaded relations, and Redis middleware shows:
- p50 latency drops from approximately 92 ms to 41 ms (55 percent reduction)
- p99 latency drops from approximately 410 ms to 185 ms (55 percent reduction)
- Throughput increases from approximately 240 req/s to 580 req/s (2.4x improvement)
The p99 reduction is the meaningful number. Cutting average latency in half is valuable, but halving the worst-case latency means your application stays responsive under load spikes, slow database queries, and cache misses. That translates directly to fewer timeout errors and a more consistent user experience.
When not to use FrankenPHP
FrankenPHP and Octane are not the right choice for every Laravel application. Skip the migration if: your application runs on shared hosting where you cannot control the process supervisor; your application has significant memory leaks that cannot be resolved through max_requests recycling; or your deployment pipeline cannot support the long-running worker model. PHP-FPM remains a perfectly viable runtime for simple sites, low-traffic applications, and environments where operational simplicity matters more than raw throughput. The performance difference only matters when your traffic volume makes framework boot time a meaningful fraction of total response time.
The one-day validation test
If you are unsure whether FrankenPHP will benefit your application, run a one-day experiment. Clone your staging environment, switch one service to octane:frankenphp with max_requests set to 500, and route real traffic through it for an afternoon. You will know within a few hours whether your application is Octane-clean or whether you have singleton state issues to resolve. That test costs less than a day of engineering time and gives you data specific to your application, not generic benchmarks.
FrankenPHP changes the Laravel deployment landscape by removing the nginx and PHP-FPM layer and replacing it with a single binary that handles HTTP serving, PHP execution, and TLS termination. The performance gains are measurable, the operational model is simpler, and the Laravel ecosystem has standardized on it as the recommended Octane backend. Deploy Laravel with FrankenPHP on a VPS, validate with your own traffic, and measure the difference in your p99 latency.
Termique manages the VPS servers where your Laravel applications run. The credential vault uses end-to-end encryption so your SSH keys never leave your devices in plaintext, and the monitoring feature tracks CPU, RAM, and disk usage across all your servers in one place. When you manage multiple VPS instances for different environments, Termique keeps your host list, SSH credentials, and deployment access organized under one interface.
Related articles