Laravel development
Laravel applications, built and kept running.
From the first model to the queue workers, the deploys and the upgrade years later - written, hosted and looked after by one team.
Laravel
PHP
Redis
Let's Encrypt
A release, as it looks on the server
Every Laravel application we run goes out the same way: tested, built, cached and switched in without a second of downtime.
01ci ~/app $ php artisan test PASS Tests\Feature\CheckoutTest ✓ a paid order queues the invoice ✓ a declined card never marks the order paid PASS Tests\Unit\VatTest ✓ vat is added once, on the total 02app ~/releases/1400 $ composer install --no-dev --optimize-autoloaderInstalling dependencies from lock fileGenerating optimized autoload files 03app ~/releases/1400 $ php artisan migrate --force INFO Running migrations. 2026_09_27_090000_add_due_date_to_invoices ....... DONE 04app ~/releases/1400 $ php artisan optimize INFO Caching framework bootstrap, configuration, and metadata. config ...................................... DONE events ...................................... DONE routes ...................................... DONE views ....................................... DONE 05app ~/releases/1400 $ ln -sfn ~/releases/1400 ~/current06app ~/releases/1400 $ php artisan horizon:terminate && php artisan octane:reload 07app ~/releases/1400 $ sudo supervisorctl statushorizon RUNNING pid 48211, uptime 0:00:09octane RUNNING pid 31877, uptime 6 days, 2:14:51 08app ~/releases/1400 $ php artisan schedule:list */5 * * * * php artisan horizon:snapshot ... Next Due: 3 minutes from now 0 2 * * * php artisan model:prune ........ Next Due: 9 hours from now 0 9 * * * php artisan invoices:remind .... Next Due: 16 hours from nowTests first
The tests run before anything reaches the server. One failing test stops the release.
Dependencies
Installed from the lock file only, without development packages, with an optimised autoloader.
Migrations
Written so the running code still works against the new schema while the release switches over.
Caches
Configuration, events, routes and views are cached. From here on the environment file is not read at all, which is why env() belongs in config files only.
The switch
One link moves and the new release is live. The previous one stays on disk for a rollback.
Workers
Horizon finishes the jobs in hand and restarts on the new code; Octane reloads its workers the same way.
Supervised
Supervisor keeps Horizon and Octane running, and brings either back if it stops.
The scheduler
One cron line runs every scheduled task. This is what it will run next.
From one click to a finished job
The slow work leaves the request. The customer gets an answer at once, and a worker does the rest in the background.
-
01 · Request
POST /checkoutThe customer pays
The controller validates the order, saves it and answers at once. Nothing slow happens here.
-
02 · Job
PayOrder::dispatch()The slow part becomes a job
Dispatched after the database commits, so a worker never picks up an order that was rolled back.
-
03 · Queue
redis · paymentsIt waits in its own queue
Payments have their own queue, so a long export never holds one up.
-
04 · Worker
horizon · supervisorA worker takes it
With a timeout, retries that wait longer each time, and a limit on how many run at once.
-
05 · Notification
OrderPaid → mail, databaseEveryone is told
The customer gets the invoice by email, and the order turns paid in your dashboard.
failed_jobs
A job that runs out of retries is kept with its error, and we are alerted. It is retried once the cause is fixed - never lost in silence.
The whole application, not only the server
Our team brings twenty-five years of PHP to every Laravel project, from the first model to the upgrade years later.
Build
The application itself.
- Architecture
- Business logic in actions and services, not in controllers. Form requests for validation, policies for who may do what.
- Eloquent
- Relations loaded up front where a page lists them, so a page costs a few queries, not hundreds. In development, lazy loading raises an error, so the problem is caught early.
- Livewire · Inertia
- Livewire when the team writes PHP and Blade; Inertia with Vue or React when the front end is a project of its own.
- APIs with Sanctum
- Tokens for mobile apps and partners, cookie sessions for your own front end, and rate limits per client.
- Pest · PHPUnit
- Tests for every path that takes money or sends mail, run on every push before a release is built.
Run
What keeps it answering.
- Queues and Horizon
- Separate queues for urgent and bulk work, each with its own workers, timeouts and retries, watched in the Horizon dashboard.
- The scheduler
- One cron line runs every task. Tasks that must not overlap, or must run on one server only, are marked that way.
- Octane
- Swoole, RoadRunner or FrankenPHP keep the application in memory between requests. Before we switch it on, we check the code for state that leaks from one request to the next.
- Redis
- Cache, sessions, queues and locks, on a Redis that is not reachable from the internet.
- Supervisor
- Queue workers, Horizon and Octane run as supervised processes, restarted if they stop and after every release.
Keep
What keeps it current.
- Zero-downtime deploys
- Each release is built in its own directory and switched in with one link. Going back is the same step in reverse.
- Logs and exceptions
- Daily log files kept for a set time, and every exception reported to us with the request that caused it.
- Upgrades
- Laravel ships a major version every year. We move an application one version at a time, with the tests passing at every step.
What we set up on day one
Before the first customer arrives, every Laravel server we run has all of this in place.
- Debug mode off in production, and only the public directory served to the web
APP_DEBUG=false - Queue workers under Supervisor, restarted on every release
supervisord - One cron line for the scheduler
* * * * * php artisan schedule:run - Redis for cache, sessions and queues
CACHE_STORE=redis - Configuration, events, routes and views cached on every release
php artisan optimize - OPcache on, and reset on every release
opcache.validate_timestamps=0 - A PHP-FPM pool sized to the memory the server has
pm.max_children - Releases in their own directories, one step from a rollback
current → releases/… - Failed jobs reported to us, not left in a table
failed_jobs - Log rotation and exception alerts
LOG_CHANNEL=daily - A nightly database backup, copied off the server
backup · offsite - A TLS certificate that renews itself
Let's Encrypt
A release on disk
/var/www/example-app
├── current → releases/1400
├── releases/
│ ├── 1400
│ └── 0930
└── shared/
├── .env
└── storage/
Four ways to serve a Laravel application
The same application can run the classic way, on PHP-FPM, or on one of the three servers that Laravel Octane supports. They differ in what they need and in what they add.
- PHP-FPM
- The classic model. The application boots again for every request.
- FrankenPHP
- A PHP application server written in Go, built on Caddy, with HTTP/3, Early Hints, and Brotli and Zstandard compression.
- RoadRunner
- An application server written in Go and run from one binary. Octane offers to download it.
- Swoole · Open Swoole
- A PHP extension, installed with PECL. Open Swoole gives the same features.
| Capability | PHP-FPM | FrankenPHP | RoadRunner | Swoole · Open Swoole |
|---|---|---|---|---|
| The application boots once and stays in memory between requests | No | Yes | Yes | Yes |
| Runs without an extra PHP extension for the server | Yes | Yes | Yes | No |
| Each request starts clean, with no shared state to check | Yes | No | No | No |
| Concurrent tasks inside one request | No | No | No | Yes |
| Ticks: code that runs every few seconds | No | No | No | Yes |
| The Octane cache and shared-memory tables | No | No | No | Yes |
A tick means the server has it; a dash means it does not. Octane is optional: a Laravel application runs on PHP-FPM as it is, and Octane adds speed by keeping it in memory, which is why the code is checked for state that leaks from one request to the next before it is switched on. Sources: Laravel Octane documentation, FrankenPHP documentation.
How long a Laravel version is supported
Laravel ships one major version a year, and each one is supported for a fixed time. This is the life of a single version, counted in months from its release, so you can see when an upgrade has to happen.
-
0months after releaseFull support
A new major version ships
Major versions arrive about once a year, in the first quarter. The minor and patch releases in between never contain breaking changes.
-
0–18months after releaseFull support
Bug fixes and security fixes
Both are provided. Every major version supports a range of PHP versions, so moving up often means moving PHP up as well.
-
12months after releaseFull support
The next major version arrives
The version you are on is still fully supported, so this is the comfortable time to plan the move.
-
18months after releaseSecurity fixes only
Bug fixes stop
From here the version receives security fixes only.
-
24months after releaseEnd of life
Security fixes stop as well
An application still on this version runs on code that nobody patches any more.
Laravel aims to make a major upgrade a matter of a day or less. We move an application one version at a time, with the tests passing at every step. Laravel support policy
Get a quote
Tell us about your application
A new build, an application that needs a home, or one stuck on an old version. Every project is quoted after a short conversation.