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.

ci ~/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 app ~/releases/1400 $ composer install --no-dev --optimize-autoloaderInstalling dependencies from lock fileGenerating optimized autoload files app ~/releases/1400 $ php artisan migrate --force   INFO  Running migrations.  2026_09_27_090000_add_due_date_to_invoices ....... DONE app ~/releases/1400 $ php artisan optimize   INFO  Caching framework bootstrap, configuration, and metadata.  config ...................................... DONE  events ...................................... DONE  routes ...................................... DONE  views ....................................... DONE app ~/releases/1400 $ ln -sfn ~/releases/1400 ~/currentapp ~/releases/1400 $ php artisan horizon:terminate && php artisan octane:reload app ~/releases/1400 $ sudo supervisorctl statushorizon                 RUNNING   pid 48211, uptime 0:00:09octane                  RUNNING   pid 31877, uptime 6 days, 2:14:51 app ~/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
An example session on an example application. Names, paths and output are illustrative.
  1. Tests first

    The tests run before anything reaches the server. One failing test stops the release.

  2. Dependencies

    Installed from the lock file only, without development packages, with an optimised autoloader.

  3. Migrations

    Written so the running code still works against the new schema while the release switches over.

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

  5. The switch

    One link moves and the new release is live. The previous one stays on disk for a rollback.

  6. Workers

    Horizon finishes the jobs in hand and restarts on the new code; Octane reloads its workers the same way.

  7. Supervised

    Supervisor keeps Horizon and Octane running, and brings either back if it stops.

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

  1. 01 · Request POST /checkout

    The customer pays

    The controller validates the order, saves it and answers at once. Nothing slow happens here.

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

  3. 03 · Queue redis · payments

    It waits in its own queue

    Payments have their own queue, so a long export never holds one up.

  4. 04 · Worker horizon · supervisor

    A worker takes it

    With a timeout, retries that wait longer each time, and a limit on how many run at once.

  5. 05 · Notification OrderPaid → mail, database

    Everyone is told

    The customer gets the invoice by email, and the order turns paid in your dashboard.

If it keeps failing 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 webAPP_DEBUG=false
  • Queue workers under Supervisor, restarted on every releasesupervisord
  • One cron line for the scheduler* * * * * php artisan schedule:run
  • Redis for cache, sessions and queuesCACHE_STORE=redis
  • Configuration, events, routes and views cached on every releasephp artisan optimize
  • OPcache on, and reset on every releaseopcache.validate_timestamps=0
  • A PHP-FPM pool sized to the memory the server haspm.max_children
  • Releases in their own directories, one step from a rollbackcurrent → releases/…
  • Failed jobs reported to us, not left in a tablefailed_jobs
  • Log rotation and exception alertsLOG_CHANNEL=daily
  • A nightly database backup, copied off the serverbackup · offsite
  • A TLS certificate that renews itselfLet's Encrypt

A release on disk

/var/www/example-app
├── current → releases/1400
├── releases/
│   ├── 1400
│   └── 0930
└── shared/
    ├── .env
    └── storage/
The web server points at current. A release is live the moment the link moves, and the one before it stays for a rollback.

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.