Running php artisan serve on your laptop is easy. Running a Laravel 13 application in production — with HTTPS, zero-downtime deploys, migrations that don't take the site down, queue workers that survive reboots and a scheduler that actually fires — is where most teams reach for a PaaS and its bill.
You don't have to. This guide walks through a production-grade Laravel 13 deployment on a single Ubuntu 24.04 server you own: first the moving parts and how to wire them by hand, then the same result with Hanoo, where a git push becomes a built, verified and reversible release.
In short: Laravel 13 requires PHP 8.3 or newer (8.4 is a good default). A production setup needs PHP-FPM, a web server pointing at
public/, a database, Composer, Node.js for Vite builds, an atomic release layout, a queue worker under a process supervisor, the scheduler running every minute, and HTTPS.
What Laravel 13 needs in production
Laravel 13 was released in March 2026 and raised the minimum PHP version to 8.3. A typical production stack looks like this:
| Piece | Recommended choice | Why |
|---|---|---|
| PHP | 8.4 (8.3–8.5 supported) | Laravel 13's minimum is 8.3; 8.4 is the mature, fast default |
| Web server | Caddy or Nginx with PHP-FPM | Serves public/, hands PHP to FPM, terminates TLS |
| Database | MariaDB / MySQL or PostgreSQL | Bound to 127.0.0.1 on a single-server setup |
| Cache, sessions, queues | Valkey (Redis-compatible) or the database |
file and sync drivers do not scale past one request |
| Assets | Node.js 22 or 24 with npm, pnpm or Yarn |
vite build produces the files in public/build
|
| Processes | Supervisor | Keeps queue:work running and restarts it on failure |
| Scheduler | Cron, every minute | Runs schedule:run, which runs your scheduled tasks |
The zero-downtime release layout
The single most important production habit is to never build in the directory that is serving traffic. Instead, every deploy gets its own directory, and a symlink decides which one is live:
/srv/app/
├── releases/
│ ├── 20261015-142210/
│ └── 20261016-091503/ ← new release, built here
├── shared/
│ ├── .env ← linked into every release
│ └── storage/ ← logs, sessions, uploads: survive deploys
└── current -> releases/20261016-091503
A deploy clones the commit into a new release directory, links shared/.env and shared/storage into it, installs dependencies, builds assets, runs migrations, and only then repoints current. Switching a symlink is atomic, so visitors either get the old release or the new one — never a half-built mix. Rolling back is pointing current at the previous release.
Option 1: deploy Laravel 13 by hand
1. Install PHP 8.4, Composer and Node.js
Ubuntu 24.04 ships PHP 8.3, which meets Laravel 13's minimum. For PHP 8.4, use the Ondřej Surý PPA:
sudo add-apt-repository ppa:ondrej/php && sudo apt update
sudo apt install php8.4-fpm php8.4-cli php8.4-mysql php8.4-mbstring php8.4-xml \
php8.4-curl php8.4-zip php8.4-bcmath php8.4-intl php8.4-redis php8.4-opcache
Install Composer 2 from getcomposer.org (verify the installer's checksum), and Node.js 22 or 24 from NodeSource or a version manager.
2. Create a deploy user and the database
Run the application as its own unprivileged user — never as root and never as www-data shared with other sites:
sudo adduser --system --group --home /srv/app laravel
Create a dedicated database and user that can connect only from 127.0.0.1.
3. Point the web server at current/public
With Nginx, the essential bits are:
server {
server_name app.example.com;
root /srv/app/current/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.4-fpm-laravel.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;
}
}
Note $realpath_root: it resolves the current symlink, so OPcache and FPM see the new release immediately after the switch instead of serving stale paths. Give the site its own PHP-FPM pool running as the laravel user, then add HTTPS with Certbot or switch to Caddy, which obtains certificates automatically.
4. Write a deploy script
The core of every Laravel deploy script:
set -euo pipefail
RELEASE=/srv/app/releases/$(date +%Y%m%d-%H%M%S)
git clone --depth 1 --branch main git@github.com:acme/shop.git "$RELEASE"
ln -s /srv/app/shared/.env "$RELEASE/.env"
rm -rf "$RELEASE/storage" && ln -s /srv/app/shared/storage "$RELEASE/storage"
cd "$RELEASE"
composer install --no-dev --prefer-dist --optimize-autoloader --no-interaction
npm ci && npm run build
php8.4 artisan migrate --force
php8.4 artisan optimize
ln -sfn "$RELEASE" /srv/app/current
sudo systemctl reload php8.4-fpm
php8.4 artisan queue:restart
Add release pruning, a health check after the switch, and a rollback path, and you have the beginnings of a deployment tool.
5. Queue workers with Supervisor
[program:laravel-queue]
command=php8.4 /srv/app/current/artisan queue:work --sleep=3 --tries=3 --max-time=3600
directory=/srv/app/current
user=laravel
numprocs=2
process_name=%(program_name)s_%(process_num)02d
autostart=true
autorestart=true
stopwaitsecs=3600
--max-time makes each worker exit periodically so Supervisor starts a fresh one, which picks up new code and releases memory. queue:restart in the deploy script tells running workers to finish their current job and exit.
6. The scheduler
* * * * * cd /srv/app/current && php8.4 artisan schedule:run >> /dev/null 2>&1
Install it in the laravel user's crontab, not root's.
Option 2: deploy Laravel 13 with Hanoo
Hanoo runs the release flow above for you, on your own server, with every step visible in the dashboard.
Step 1: connect your server
Run the one-time install command from Connect server on a clean Ubuntu 22.04, 24.04 or 26.04 LTS server. The agent connects out over mutually authenticated TLS — no inbound management port, no root password shared — and you approve the server in the dashboard. Laravel applications need at least 1 CPU core and 2 GB of RAM.
Step 2: create the application from your repository
Choose Create application, select Laravel and point it at your repository on GitHub, GitLab or Bitbucket. The server generates a read-only deploy key; you add its public half to the repository, and the private half never leaves your server. Pick PHP 8.4 (8.3–8.5 are available) and tick the database option if you want a private MariaDB database.
Hanoo creates an isolated Linux user and PHP-FPM pool for the application, and writes its .env from your .env.example — with APP_ENV=production, APP_DEBUG=false, APP_URL set to the application's address, the database credentials filled in when you asked for a database, and a freshly generated APP_KEY. That file lives in the application's shared directory and is linked into every release.
Step 3: what happens on every deploy
Each deploy:
- Fetches the selected commit into a new, immutable release.
- Detects your toolchain from the repository —
composer.lockmeans Composer;pnpm-lock.yaml,package-lock.jsonoryarn.lockpicks the JavaScript package manager — and installs checksum-verified versions of Composer and Node.js (20, 22 or 24). - Runs
composer installand your asset build as the application's own user. - Links the shared
.envandstorage/into the release and validates it. - Runs your deploy pipeline — steps such as
php artisan migrate --force, cache warm-up or queue restarts, grouped into build, before activation and after activation phases. - Activates the release atomically, reloads PHP-FPM, and checks the site responds. A failed health check reverts to the previous release automatically.
Every step is timed and its output is shown, so a failed build tells you why without an SSH session. A failed build never touches the running site, and any retained release can be restored with one-click rollback.
Step 4: domain and HTTPS
Add app.example.com under the application's Domains tab. Hanoo checks that DNS points at your server, issues a Let's Encrypt certificate and switches the site over with zero downtime.
Step 5: queues and the scheduler
The default .env uses QUEUE_CONNECTION=sync, which runs jobs inside the web request. For real background processing:
- Install Valkey from the server's Manage Services (it is Redis-compatible, so Laravel's
redisdriver works unchanged), or use thedatabasequeue driver. Then setQUEUE_CONNECTIONand, if you use Valkey,CACHE_STOREandSESSION_DRIVERin.envover SSH as the application user. - Add a Supervisor program for the application running
php8.4 artisan queue:work --sleep=3 --tries=3 --max-time=3600. Programs run as the application's user in its current release; you can start, stop and restart them and read their logs from the dashboard. - Add an entry under the application's Cron jobs tab running
php8.4 artisan schedule:runevery minute. You can run it on demand and see the latest result and output.
Production checklist for Laravel 13
-
APP_ENV=productionandAPP_DEBUG=false— debug mode leaks environment variables in error pages. -
php artisan optimizeafter each deploy to cache config, routes, views and events. - OPcache enabled; with an atomic symlink layout, resolve paths with
realpathso new releases are picked up. - Sessions, cache and queues on Valkey/Redis or the database — not
fileonce you have more than one worker. -
storage/and.envoutside the release directory, shared across releases. - Migrations that are backwards compatible with the previous release, so a rollback does not break.
- Queue workers restarted on every deploy, with
--max-timeas a safety net. - Scheduler running every minute as the application user.
- Off-server backups of the database and
storage/app.
Frequently asked questions
What PHP version does Laravel 13 require?
Laravel 13 requires PHP 8.3 or newer and supports PHP 8.3, 8.4 and 8.5. PHP 8.4 is a sensible production default today.
How do I deploy Laravel without downtime?
Build each release in its own directory, share .env and storage/ between releases, run migrations before the switch, then atomically repoint a current symlink at the new release and reload PHP-FPM. Hanoo does exactly this on every deploy and reverts automatically if the new release fails its health check.
Do I need Laravel Forge or a PaaS to host Laravel?
No. Any Ubuntu server with PHP-FPM, a web server, a database and Supervisor can run Laravel in production. Tools like Hanoo automate the setup and the release flow while the server and code stay entirely yours.
How do I run Laravel queue workers in production?
Run php artisan queue:work under a process supervisor such as Supervisor, as the application's user, with --max-time so workers recycle, and call php artisan queue:restart after each deploy so they load the new code.
How do I run the Laravel scheduler on a server?
Add one cron entry that runs php artisan schedule:run every minute from the application's current directory. Laravel decides which scheduled tasks are due on each run.
Can I use Valkey instead of Redis with Laravel?
Yes. Valkey is an open-source fork of Redis that speaks the same protocol, so Laravel's redis cache, session and queue drivers work with it unchanged.
Originally published on the Hanoo blog.
Top comments (0)