Skip to content

Deploying Laravel Yourself, Without the Two-Minute Outage

A managed platform, a provisioning service or a plain server - and the four things that decide whether a deploy is invisible, none of which are the hosting choice.

4 min read

There are three reasonable ways to run a Laravel application, and the argument about which one usually hides the fact that the deploy script matters more than the choice.

The three options, honestly

A managed platform. You push, it builds and runs. The least operational work available, at the highest per-unit price, with the platform's constraints as your constraints. Good for a team with no operations capacity and a workload that fits.

A provisioning service on your own servers. Something that configures a server you own and gives you deploys, certificates and workers without you writing the Ansible. This is where most Laravel applications live and it is a sensible default: you keep the machine, you skip the setup.

A plain server you configure. Cheapest, most control, and it is only cheap if someone owns it. Updates, certificates, backups, monitoring - the invoice is small and the responsibility is not.

There is no wrong answer here. There is only an unowned one.

What actually decides whether a deploy hurts

1. Atomic releases. Deploy into a new directory and move a symlink when it is ready. Copying files over a running application means requests are served by a half-updated codebase for a few seconds, which produces errors nobody can reproduce afterwards.

releases/2026-09-21-140233/
current -> releases/2026-09-21-140233

Shared between releases: storage/ and the .env. Everything else is new each time, and rollback is moving the symlink back.

2. The cache rebuild, in order.

php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache

Run these on the new release before it becomes current. config:cache in particular means env() returns null everywhere outside config files - which is correct behaviour and surprises people, so keep env() in config files only.

3. Restarting queue workers. The step most often missing:

php artisan queue:restart

A worker boots your application once and keeps it. Without this, your web tier runs the new release and your queue tier runs whatever was current when those processes last started - which is its own class of bug.

4. Migrations, separated by risk. Routine ones in the deploy. Anything that rewrites a large table run deliberately and watched, because the lock outlasts the request timeout and the deploy will report success while the site returns errors.

The order that keeps the site up

# in the new release directory, before it is live
composer install --no-dev --optimize-autoloader
npm ci && npm run build
php artisan config:cache && php artisan route:cache && php artisan view:cache
 
# make it current
ln -sfn "$RELEASE" current
 
# after
php artisan migrate --force
php artisan queue:restart
sudo systemctl reload php8.3-fpm

Reload rather than restart, so in-flight requests finish. And check afterwards rather than trusting the script:

ps -eo lstart,cmd | grep "[q]ueue:work"

Start times older than the deploy mean the restart did not reach them.

The pieces people forget to set up at all

The scheduler needs one cron entry, and only one, on one machine:

* * * * * cd /var/www/app && php artisan schedule:run >> /dev/null 2>&1

Two servers both running it means every scheduled task runs twice.

Workers need a supervisor. queue:restart stops workers; it does not start them. Without systemd or Supervisor watching, a deploy quietly leaves you with none and jobs pile up until somebody notices.

Backups need a restore. A backup that has never been restored is a hypothesis. Restore one into a scratch environment, on a schedule, and write down how long it took - that number is your actual recovery time.

Logs need somewhere to go. The default daily file on the application server is fine until you have two servers, at which point an incident means reading two sets of files by hand.

The part that is not about hosting

Nothing above depends on which of the three options you chose. A managed platform handles some of it for you; a plain server means you write it once. Either way the deploy either restarts the workers or it does not.

Which is why the most useful thing to do with this article is not to change hosting. It is to read your own deploy script and find which of these four steps is missing - in our experience it is usually the worker restart, and it has been missing since the day the script was written.

Related questions

Is a plain VPS a bad idea?
No, and for a single application it is often the cheapest thing that works well. What makes it a bad idea is nobody owning it - security updates, certificate renewal, backups that have been restored at least once. Those are the running cost, not the monthly invoice.
Should migrations run automatically during a deploy?
The routine ones, yes, because a manual step is a step somebody eventually forgets. The exception is anything that rewrites a large table, which wants running deliberately and watched - and separated from the deploy that depends on it.
Do we need containers?
Only if you have a reason. Containers solve environment drift across several services and several machines; for one Laravel application on one server they add a build pipeline and a registry to maintain. Adopt them when the problem they solve is one you actually have.
What is the most commonly missed step?
Restarting queue workers. A worker holds the code it booted with, so a deploy that does not restart them leaves half your application running last week's release - and the resulting bugs look intermittent and unreproducible.

← Back to all articles

Call us+1 848 272 7583WhatsApp+90 850 308 5436Emailinfo@codefacture.comContact page