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.
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 now
Tests 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/

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.